'use strict';
'require form';
'require fs';
'require ui';
'require dockerman.common as dm2';

/*
Copyright 2026
Docker manager JS for Luci by Paul Donald <newtwen+github@gmail.com> 
Based on Docker Lua by lisaac <https://github.com/lisaac/luci-app-dockerman>
LICENSE: GPLv2.0
*/

/* API v1.52

POST /containers/create no longer supports configuring a container-wide MAC
address via the container's Config.MacAddress field. A container's MAC address
can now only be configured via endpoint settings when connecting to a network.

*/

return dm2.dv.extend({
	load() {
		const requestPath = L.env.requestpath;
		const duplicateId = requestPath[requestPath.length-1];
		const isDuplicate = requestPath[requestPath.length-2] === 'duplicate' && duplicateId;

		const promises = [
			dm2.image_list().then(images => {
				return images.body || [];
			}),
			dm2.network_list().then(networks => {
				return networks.body || [];
			}),
			dm2.volume_list().then(volumes => {
				return volumes.body?.Volumes || [];
			}),
			dm2.docker_info().then(info => {
				const numcpus = info.body?.NCPU || 1.0;
				const memory = info.body?.MemTotal || 2**10;
				return {numcpus: numcpus, memory: memory};
			}),
		];

		if (isDuplicate) {
			promises.push(
				dm2.container_inspect({ id: duplicateId }).then(container => {
					this.duplicateContainer = container.body || {};
					this.isDuplicate = true;
				})
			);
		}

		return Promise.all(promises);
	},

	render([image_list, network_list, volume_list, cpus_mem]) {
		this.volumes = volume_list;
		const view = this; // Capture the view context

		// Load duplicate container config if available
		let containerData = {container: {}};
		let pageTitle = _('Create new docker container');

		if (this.isDuplicate && this.duplicateContainer) {
			pageTitle = _('Duplicate/Edit Container: %s').format(this.duplicateContainer.Name?.substring(1) || '');
			const resolveImageId = (imageRef) => {
				if (!imageRef) return null;
				const match = (image_list || []).find(img => img.Id === imageRef || (Array.isArray(img.RepoTags) && img.RepoTags.includes(imageRef)));
				return match?.Id || null;
			};
			const c = this.duplicateContainer;
			const hostConfig = c.HostConfig || {};
			const resolvedImage = resolveImageId(c.Image) || resolveImageId(c.Config?.Image) || c.Image || c.Config?.Image || '';
			const builtInNetworks = new Set(['none', 'bridge', 'host']);
			// Object.entries returns [[name, obj], ...] — pick the first network
			const [[firstName, firstNet] = []] = Object.entries(c.NetworkSettings?.Networks || {});

			containerData.container = {
				name: c.Name?.substring(1) || '',
				interactive: c.Config?.AttachStdin ? 1 : 0,
				tty: c.Config?.Tty ? 1 : 0,
				image: resolvedImage,
				privileged: hostConfig.Privileged ? 1 : 0,
				restart_policy: hostConfig.RestartPolicy?.Name || 'unless-stopped',
				network: firstName || '',
				network_aliases: (() => {
					if (!firstName || builtInNetworks.has(firstName)) return '';
					return (firstNet?.Aliases || []).join(', ');
				})(),
				ipv4: (() => {
					if (!firstName || builtInNetworks.has(firstName)) return '';
					return firstNet?.IPAddress || '';
				})(),
				ipv6: (() => {
					if (!firstName || builtInNetworks.has(firstName)) return '';
					return firstNet?.GlobalIPv6Address || '';
				})(),
				ipv6_lla: (() => {
					if (!firstName || builtInNetworks.has(firstName)) return '';
					return firstNet?.LinkLocalIPv6Address || '';
				})(),
				link: hostConfig.Links || [],
				dns: hostConfig.Dns || [],
				user: c.Config?.User || '',
				env: c.Config?.Env || [],
				volume: (hostConfig.Mounts || c.Mounts || []).map(m => {
					let source;
					const destination = m.Destination || m.Target || '';
					let opts = '';
					if (m.Type === 'image') {
						source = '@image';
						if (m.ImageOptions && m.ImageOptions.Subpath)
							opts = 'subpath=' + m.ImageOptions.Subpath;
					} else if (m.Type === 'tmpfs') {
						source = '@tmpfs';
						const tmpOpts = [];
						if (m.TmpfsOptions) {
							if (m.TmpfsOptions.SizeBytes) tmpOpts.push('size=' + m.TmpfsOptions.SizeBytes);
							if (m.TmpfsOptions.Mode) tmpOpts.push('mode=' + m.TmpfsOptions.Mode);
							if (Array.isArray(m.TmpfsOptions.Options)) {
								for (const o of m.TmpfsOptions.Options) {
									if (Array.isArray(o) && o.length === 2) tmpOpts.push(`${o[0]}=${o[1]}`);
									else if (Array.isArray(o) && o.length === 1) tmpOpts.push(o[0]);
								}
							}
						}
						opts = tmpOpts.join(',');
					} else if (m.Type === 'volume') {
						source = m.Source || '';
						// opts = m.Mode || '';
					} else {
						source = m.Source || '';
						opts = m.Mode || '';
					}
					return source + ':' + destination + (opts ? ':' + opts : '');
				}),
				publish: (() => {
					const ports = [];
					for (const [containerPort, bindings] of Object.entries(hostConfig.PortBindings || {})) {
						if (Array.isArray(bindings) && bindings.length > 0) {
							for (const b of bindings) {
								if (!b?.HostPort) continue;
								const ip = (b.HostIp && b.HostIp !== '0.0.0.0' && b.HostIp !== '::') ? b.HostIp : '';
								ports.push((ip ? ip + ':' : '') + b.HostPort + ':' + containerPort);
							}
						}
					}
					return ports;
				})(),
				command: c.Config?.Cmd ? c.Config?.Cmd.map(arg => {
					if (arg.includes(' ') || arg.includes('"') || arg.includes("'")) {
						return '"' + arg.replace(/"/g, '\\"') + '"';
					}
					return arg;
				}).join(' ') : '',
				hostname: c.Config?.Hostname || '',
				publish_all: hostConfig.PublishAllPorts ? 1 : 0,
				device: (hostConfig.Devices || []).map(d => d.PathOnHost + ':' + d.PathInContainer + (d.CgroupPermissions ? ':' + d.CgroupPermissions : '')),
				tmpfs: (() => {
					const list = [];
					if (hostConfig.Tmpfs && typeof hostConfig.Tmpfs === 'object') {
						for (const [path, opts] of Object.entries(hostConfig.Tmpfs)) {
							list.push(path + (opts ? ':' + opts : ''));
						}
					}
					return list;
				})(),
				sysctl: (() => {
					const list = [];
					if (hostConfig.Sysctls && typeof hostConfig.Sysctls === 'object') {
						for (const [key, value] of Object.entries(hostConfig.Sysctls)) {
							list.push(key + ':' + value);
						}
					}
					return list;
				})(),
				cap_add: hostConfig.CapAdd || [],
				cpus: hostConfig.NanoCPUs ? (hostConfig.NanoCPUs / (10 ** 9)).toFixed(3) : '',
				cpu_shares: hostConfig.CpuShares || '',
				cpu_period: hostConfig.CpuPeriod || '',
				cpu_quota: hostConfig.CpuQuota || '',
				memory: hostConfig.Memory || '',
				memory_reservation: hostConfig.MemoryReservation || '',
				blkio_weight: hostConfig.BlkioWeight || '',
				log_opt: (() => {
					const list = [];
					const logConfig = hostConfig.LogConfig?.Config;
					if (logConfig && typeof logConfig === 'object') {
						for (const [key, value] of Object.entries(logConfig)) {
							list.push(key + '=' + value);
						}
					}
					return list;
				})(),
				log_driver: hostConfig.LogConfig?.Type || '',
			};
		}

		// stuff JSONMap with container config
		const m = new form.JSONMap(containerData, _('Docker - Containers'));
		m.submit = true;
		m.reset = true;

		let s = m.section(form.NamedSection, 'container', pageTitle);
		s.anonymous = true;
		s.nodescriptions = true;
		s.addremove = false;

		let o;

		o = s.option(form.Value, 'name', _('Container Name'),
			_('Name of the container that can be selected during container creation'));
		o.rmempty = true;

		o = s.option(form.Flag, 'interactive', _('Interactive (-i)'));
		o.rmempty = true;
		o.disabled = 0;
		o.enabled = 1;
		o.default = 0;

		o = s.option(form.Flag, 'tty', _('TTY (-t)'));
		o.rmempty = true;
		o.disabled = 0;
		o.enabled = 1;
		o.default = 0;

		o = s.option(form.ListValue, 'image', _('Docker Image'));
		o.rmempty = true;
		for (const image of image_list) {
			o.value(image.Id, image?.RepoTags?.[0]);
		}

		o = s.option(form.Flag, 'pull', _('Always pull image first'));
		o.rmempty = true;
		o.disabled = 0;
		o.enabled = 1;
		o.default = 0;

		o = s.option(form.Flag, 'privileged', _('Privileged'));
		o.rmempty = true;
		o.disabled = 0;
		o.enabled = 1;
		o.default = 0;

		o = s.option(form.ListValue, 'restart_policy', _('Restart Policy'));
		o.rmempty = true;
		o.default = 'unless-stopped';
		o.value('no', _('No'));
		o.value('unless-stopped', _('Unless stopped'));
		o.value('always', _('Always'));
		o.value('on-failure', _('On failure'));

		o = s.option(form.ListValue, 'network', _('Networks'));
		o.rmempty = true;
		this.buildNetworkListValues(network_list, o);

		function not_with_a_docker_net(section_id, value) {
			if (!value || value === "") return true;
			const builtInNetworks = new Set(['none', 'bridge', 'host']);
			let dnet = this.section.getOption('network').getUIElement(section_id).getValue();
			const disallowed = builtInNetworks.has(dnet);
			if (disallowed) return _('Only for user-defined networks');
			return true;
		};

		o = s.option(form.Value, 'ipv4', _('IPv4 Address'));
		o.rmempty = true;
		o.datatype = 'ip4addr';
		o.validate = not_with_a_docker_net;

		o = s.option(form.Value, 'ipv6', _('IPv6 Address'));
		o.rmempty = true;
		o.datatype = 'ip6addr';
		o.validate = not_with_a_docker_net;

		o = s.option(form.Value, 'ipv6_lla', _('IPv6 Link-Local Address'));
		o.rmempty = true;
		o.datatype = 'ip6ll';
		o.validate = not_with_a_docker_net;

		o = s.option(form.Value, 'network_aliases', _('Network Aliases'));
		o.rmempty = true;
		o.placeholder = 'database,db (CSV)';
		o.validate = not_with_a_docker_net;

		o = s.option(form.DynamicList, 'link', _('Links with other containers'));
		o.rmempty = true;
		o.placeholder='container_name:alias';

		o = s.option(form.DynamicList, 'dns', _('Set custom DNS servers'));
		o.rmempty = true;
		o.placeholder='8.8.8.8';

		o = s.option(form.Value, 'user', _('User(-u)'),
			_('The user that commands are run as inside the container. (format: name|uid[:group|gid])'));
		o.rmempty = true;
		o.placeholder='1000:1000';

		o = s.option(form.DynamicList, 'env', _('Environmental Variable(-e)'),
			_('Set environment variables inside the container'));
		o.rmempty = true;
		o.placeholder='TZ=Europe/Paris';

		o = s.option(form.DummyValue, 'volume', _('Mount(--mount)'),
			_('Bind mount a volume'));
		o.rmempty = true;
		o.cfgvalue = () => {
			const c_volumes = view.map.data.get('json', 'container', 'volume') || [];

			const showVolumeModal = (index, initialEntry) => {
				let typeSelect, bindPicker, bindSourceField, volumeNameInput, volumeSourceField, pathInput, pathField, optionsDropdown, optionsField, subpathInput;
				let tmpfsSizeInput, tmpfsModeInput, tmpfsOptsInput, tmpfsSizeField, tmpfsModeField, tmpfsOptsField;
				const isEdit = index !== null;
				const modalTitle = isEdit ? _('Edit Mount') : _('Add Mount');

					// Parse existing entry if editing and infer type from volumes list, image, or tmpfs
					let initialType = 'bind', initialSource = '', initialPath = '', initialOptions = '';
					if (isEdit && initialEntry) {
						const parts = (typeof initialEntry === 'string' ? initialEntry : '').split(':');
						initialSource = parts[0] || '';
						initialPath = parts[1] || '';
						initialOptions = parts[2] || '';
						// Infer type: tmpfs, volume, image, else bind
						const isTmpfs = (initialSource === '@tmpfs');
						const isVolume = (volume_list || []).some(v => v.Name === initialSource || v.Id === initialSource);
						const isImage = (initialSource === '@image');
						initialType = isTmpfs ? 'tmpfs' : (isVolume ? 'volume' : (isImage ? 'image' : 'bind'));
				}

				const existingOptions = (typeof initialOptions === 'string' ? initialOptions : '').split(',').map(o => o.trim()).filter(Boolean);

				// Type-specific options for dropdowns
				const bindOptions = {
					'ro': _('Read-only (ro)'),
					'rw': _('Read-write (rw)'),
					'private': _('Propagation: private'),
					'rprivate': _('Propagation: rprivate'),
					'shared': _('Propagation: shared'),
					'rshared': _('Propagation: rshared'),
					'slave': _('Propagation: slave'),
					'rslave': _('Propagation: rslave')
				};

				const volumeOptions = {
					// 'ro': _('Read-only (ro)'),
					// 'rw': _('Read-write (rw)'),
					'nocopy': _('No copy (nocopy)')
				};

				const getOptionsForType = (type) => type === 'bind' ? bindOptions : volumeOptions;

				const namesListId = 'volname-list-' + Math.random().toString(36).substr(2, 9);

				// Create dropdown for options - updates based on type
				optionsDropdown = new ui.Dropdown(existingOptions, getOptionsForType(initialType), {
					id: 'mount-options-' + Math.random().toString(36).substr(2, 9),
					multiple: true,
					optional: true,
					display_items: 2,
					placeholder: _('Select options...')
				});

				const createField = (label, input) => {
					return E('div', { 'class': 'cbi-value' }, [
						E('label', { 'class': 'cbi-value-title' }, label),
						E('div', { 'class': 'cbi-value-field' }, Array.isArray(input) ? input : [input])
					]);
				};

				// Type select
				const typeOptions = [
					E('option', { value: 'bind' }, _('Bind (host directory)')),
					E('option', { value: 'volume' }, _('Volume (named)')),
					E('option', { value: 'image' }, _('Image (from image)')),
					E('option', { value: 'tmpfs' }, _('Tmpfs'))
				];
				typeSelect = E('select', { 'class': 'cbi-input-select' }, typeOptions);
				typeSelect.value = initialType;

				// Bind directory picker using ui.FileUpload
				bindPicker = new ui.FileUpload(initialType === 'bind' ? initialSource : '', {
					browser: false,
					directory_select: true,
					directory_create: false,
					enable_upload: false,
					enable_remove: false,
					enable_download: false,
					root_directory: '/',
					show_hidden: true
				});

				// Volume name input with datalist
				volumeNameInput = E('input', {
					'type': 'text',
					'class': 'cbi-input-text',
					'placeholder': _('Enter volume name or pick existing'),
					'list': namesListId,
					'value': initialType === 'volume' ? initialSource : ''
				});
				volumeSourceField = createField(_('Volume Name'), [
					E('div', { 'style': 'position: relative;' }, [
						volumeNameInput,
						E('span', { 'style': 'pointer-events: none;' }, '▼')
					]),
					E('datalist', { 'id': namesListId }, [
						...volume_list.map(vol => E('option', { 'value': vol.Name }, vol.Name))
					])
				]);

				// Tmpfs inputs - pre-populate if editing
				let tmpfsSizeVal = '', tmpfsModeVal = '', tmpfsOptsVal = '';
				if (initialType === 'tmpfs' && existingOptions.length) {
					const rest = [];
					existingOptions.forEach(o => {
						if (o.startsWith('size=')) tmpfsSizeVal = o.slice('size='.length);
						else if (o.startsWith('mode=')) tmpfsModeVal = view.modeToRwx(o.slice('mode='.length));
						else rest.push(o);
					});
					tmpfsOptsVal = rest.join(',');
				}

				tmpfsSizeField = createField(_('Size'),
					tmpfsSizeInput = E('input', {
						'class': 'cbi-input-text',
						'placeholder': '128m',
						'value': tmpfsSizeVal
					})
				);
				tmpfsModeField = createField(_('Mode'),
					tmpfsModeInput = E('input', {
						'class': 'cbi-input-text',
						'placeholder': 'rwxr-xr-x or 1770',
						'value': tmpfsModeVal
					})
				);
				tmpfsOptsField = createField(_('tmpfs Options'),
					tmpfsOptsInput = E('input', {
						'type': 'text',
						'class': 'cbi-input-text',
						'placeholder': 'nr_blocks=blocks,...',
						'value': tmpfsOptsVal
					})
				);

				// Render bindPicker and show modal
				Promise.resolve(bindPicker.render()).then(bindPickerNode => {
					bindSourceField = createField(_('Host Directory'), bindPickerNode);

					const updateOptions = (selectedType) => {
						optionsField.querySelector('.cbi-value-field').innerHTML = '';
						if (selectedType === 'image') {
							// For image mounts, show a Subpath text input (only option)
							subpathInput = E('input', {
								'type': 'text',
								'class': 'cbi-input-text',
								'placeholder': _('/path/in/image'),
								'value': (initialType === 'image' && existingOptions.find(o => o.startsWith('subpath='))) ? existingOptions.find(o => o.startsWith('subpath=')).slice('subpath='.length) : ''
							});
							optionsField.querySelector('.cbi-value-title').textContent = _('Subpath');
							optionsField.querySelector('.cbi-value-field').appendChild(subpathInput);
						} else if (selectedType === 'tmpfs') {
							// Tmpfs fields are shown as main fields, hide options field
							optionsField.style.display = 'none';
						} else {
							optionsField.querySelector('.cbi-value-title').textContent = _('Options');
							// Recreate dropdown with new options
							const currentValue = optionsDropdown.getValue();
							optionsDropdown = new ui.Dropdown(currentValue, getOptionsForType(selectedType), {
								id: 'mount-options-' + Math.random().toString(36).substr(2, 9),
								multiple: true,
								optional: true,
								display_items: 2,
								placeholder: _('Select options...')
							});
							optionsField.querySelector('.cbi-value-field').appendChild(optionsDropdown.render());
							optionsField.style.display = '';
						}
					};

					const toggleSources = () => {
						const isBind = typeSelect.value === 'bind';
						const isVolume = typeSelect.value === 'volume';
						const isImage = typeSelect.value === 'image';
						const isTmpfs = typeSelect.value === 'tmpfs';
						bindSourceField.style.display = isBind ? '' : 'none';
						volumeSourceField.style.display = isVolume ? '' : 'none';
						pathField.style.display = isImage ? 'none' : '';
						tmpfsSizeField.style.display = isTmpfs ? '' : 'none';
						tmpfsModeField.style.display = isTmpfs ? '' : 'none';
						tmpfsOptsField.style.display = isTmpfs ? '' : 'none';
						updateOptions(typeSelect.value);
					};

					optionsField = createField(_('Options'), optionsDropdown.render());

					ui.showModal(modalTitle, [
						createField(_('Type'), typeSelect),
						bindSourceField,
						volumeSourceField,
						pathField = createField(_('Mount Path'),
							pathInput = E('input', {
								'type': 'text',
								'class': 'cbi-input-text',
								'placeholder': _('/mnt/path'),
								'value': initialPath
							})
						),
						tmpfsSizeField,
						tmpfsModeField,
						tmpfsOptsField,
						optionsField,
						E('div', { 'class': 'right' }, [
							E('button', {
								'class': 'cbi-button',
								'click': ui.hideModal
							}, [_('Cancel')]),
							' ',
							E('button', {
								'class': 'cbi-button cbi-button-positive',
								'click': ui.createHandlerFn(view, () => {
									const selectedType = typeSelect.value;
									const sourcePath = selectedType === 'bind'
										? (bindPicker.getValue() || '').trim()
										: (selectedType === 'volume'
											? (volumeNameInput.value || '').trim()
											: (selectedType === 'tmpfs' ? '@tmpfs' : '@image'));
									const subpathVal = (selectedType === 'image') ? (subpathInput?.value || '').trim() : '';
									const mountPath = (selectedType === 'image') ? subpathVal : pathInput.value.trim();
									let selectedOptions;
									if (selectedType === 'image') {
										selectedOptions = subpathVal ? ('subpath=' + subpathVal) : '';
									} else if (selectedType === 'tmpfs') {
										const opts = [];
										const sizeValRaw = (tmpfsSizeInput?.value || '').trim();
										const modeValRaw = (tmpfsModeInput?.value || '').trim();
										const extraVal = (tmpfsOptsInput?.value || '').trim();
										const parsedSize = sizeValRaw ? view.parseMemory(sizeValRaw) : undefined;
										const parsedMode = view.rwxToMode(modeValRaw);
										if (parsedSize) opts.push('size=' + parsedSize);
										else if (sizeValRaw) opts.push('size=' + sizeValRaw); // fallback if parse fails
										if (parsedMode !== undefined) opts.push('mode=' + parsedMode);
										if (extraVal) opts.push(...extraVal.split(',').map(o => o.trim()).filter(Boolean));
										selectedOptions = opts.join(',');
									} else {
										selectedOptions = optionsDropdown.getValue().join(',');
									}

									if (!sourcePath) {
										ui.addTimeLimitedNotification(null, [_('Please choose a directory or enter a volume name')], 3000, 'warning');
										return;
									}

									if (selectedType !== 'image' && !mountPath) {
										ui.addTimeLimitedNotification(null, [_('Please enter a mount path')], 3000, 'warning');
										return;
									}
									if (selectedType === 'image' && !subpathVal) {
										ui.addTimeLimitedNotification(null, [_('Please enter a subpath')], 3000, 'warning');
										return;
									}
									if (selectedType === 'tmpfs' && !mountPath) {
										ui.addTimeLimitedNotification(null, [_('Please enter a mount path')], 3000, 'warning');
										return;
									}

									ui.hideModal();

									const currentVolumes = view.map.data.get('json', 'container', 'volume') || [];
									const volumeEntry = selectedOptions ? (sourcePath + ':' + mountPath + ':' + selectedOptions) : (sourcePath + ':' + mountPath);
									let updatedVolumes;
									if (isEdit) {
										updatedVolumes = [...currentVolumes];
										updatedVolumes[index] = volumeEntry;
									} else {
										updatedVolumes = Array.isArray(currentVolumes) ? [...currentVolumes, volumeEntry] : [volumeEntry];
									}
									view.map.data.set('json', 'container', 'volume', updatedVolumes);

									return view.map.render();
								})
							}, [isEdit ? _('Update') : _('Add')])
						])
					]);

					toggleSources();
					typeSelect.addEventListener('change', toggleSources);
				});
			};


			return E('div', { 'class': 'cbi-dynlist' }, [
				...(c_volumes.length > 0 ? c_volumes.map((v, idx) => E('div', {
					'class': 'cbi-dynlist-item',
					'style': 'display: flex; justify-content: space-between; align-items: center; padding: 8px 5px; margin-bottom: 8px; gap: 10px;'
				}, [
					E('span', {
						'style': 'cursor: pointer; flex: 1;',
						'click': ui.createHandlerFn(view, () => {
							showVolumeModal(idx, v);
						})
					}, v),
					E('button', {
						'style': 'padding: 5px; color: #c44;',
						'class': 'cbi-button-negative remove',
						'title': _('Delete this volume mount'),
						'click': ui.createHandlerFn(view, () => {
							const currentVolumes = view.map.data.get('json', 'container', 'volume') || [];
							const updatedVolumes = currentVolumes.filter((_, i) => i !== idx);
							view.map.data.set('json', 'container', 'volume', updatedVolumes);
							return view.map.render();
						})
					}, ['✕'])
				])) : [E('div', { 'style': 'padding: 5px; color: #999;' }, _('No volumes available'))]),
				E('button', {
					'class': 'cbi-button',
					'click': ui.createHandlerFn(view, () => {
						showVolumeModal(null, null);
					})
				}, [_('Add Mount')])
			]);
		};
		o.rmempty = true;

		o = s.option(form.DynamicList, 'publish', _('Exposed Ports(-p)'),
			_("Publish container's port(s) to the host"));
		o.rmempty = true;
		o.placeholder='2200:22/tcp';

		o = s.option(form.Value, 'command', _('Run command'));
		o.rmempty = true;
		o.placeholder='/bin/sh init.sh';

		o = s.option(form.Flag, 'advanced', _('Advanced'));
		o.rmempty = true;
		o.disabled = 0;
		o.enabled = 1;
		o.default = 0;

		o = s.option(form.Value, 'hostname', _('Host Name'),
			_('The hostname to use for the container'));
		o.rmempty = true;
		o.placeholder='/bin/sh init.sh';
		o.depends('advanced', 1);

		o = s.option(form.Flag, 'publish_all', _('Exposed All Ports(-P)'),
			_("Allocates an ephemeral host port for all of a container's exposed ports"));
		o.rmempty = true;
		o.disabled = 0;
		o.enabled = 1;
		o.default = 0;
		o.depends('advanced', 1);

		o = s.option(form.DynamicList, 'device', _('Device(--device)'),
			_('Add host device to the container'));
		o.rmempty = true;
		o.placeholder='/dev/sda:/dev/xvdc:rwm';
		o.depends('advanced', 1);

		o = s.option(form.DynamicList, 'tmpfs', _('Tmpfs(--tmpfs)'),
			_('Mount tmpfs directory'));
		o.rmempty = true;
		o.placeholder='/run:rw,noexec,nosuid,size=65536k';
		o.depends('advanced', 1);

		o = s.option(form.DynamicList, 'sysctl', _('Sysctl(--sysctl)'),
			_('Sysctls (kernel parameters) options'));
		o.rmempty = true;
		o.placeholder='net.ipv4.ip_forward=1';
		o.depends('advanced', 1);

		o = s.option(form.DynamicList, 'cap_add', _('CAP-ADD(--cap-add)'),
			_('A list of kernel capabilities to add to the container'));
		o.rmempty = true;
		o.placeholder='NET_ADMIN';
		o.depends('advanced', 1);

		o = s.option(form.Value, 'cpus', _('CPUs'),
			_('Number of CPUs. Number is a fractional number. 0.000 means no limit'));
		o.rmempty = true;
		o.placeholder='1.5';
		o.datatype = 'ufloat';
		o.depends('advanced', 1);
		o.validate = function(section_id, value) {
			if (!value) return true;
			if (value > cpus_mem.numcpus) return _(`Only ${cpus_mem.numcpus} CPUs available`);
			return true;
		};

		o = s.option(form.Value, 'cpu_period', _('CPU Period'),
			_('The length of a CPU period in microseconds'));
		o.rmempty = true;
		o.datatype = 'or(and(uinteger,min(1000),max(1000000)),"0")';
		o.depends('advanced', 1);

		o = s.option(form.Value, 'cpu_quota', _('CPU Quota'),
			_('Microseconds of CPU time that the container can get in a CPU period'));
		o.rmempty = true;
		o.datatype = 'uinteger';
		o.depends('advanced', 1);

		o = s.option(form.Value, 'cpu_shares', _('CPU Shares Weight'),
			_('CPU shares relative weight, if 0 is set, the system will ignore the value and use the default of 1024'));
		o.rmempty = true;
		o.placeholder='1024';
		o.datatype = 'uinteger';
		o.depends('advanced', 1);

		o = s.option(form.Value, 'memory', _('Memory'),
			_('Memory limit (format: <number>[<unit>]). Number is a positive integer. Unit can be one of b, k, m, or g. Minimum is 4M'));
		o.rmempty = true;
		o.placeholder = '128m';
		o.depends('advanced', 1);
		o.write = function(section_id, value) {
			if (!value || value == 0) return 0;
			this.map.data.data[section_id].memory = view.parseMemory(value);;
			return view.parseMemory(value);
		};
		o.validate = function(section_id, value) {
			if (!value) return true;
			if (value > view.memory) return _(`Only ${view.memory} bytes available`);
			return true;
		};

		o = s.option(form.Value, 'memory_reservation', _('Memory Reservation'));
		o.depends('advanced', 1);
		o.placeholder = '128m';
		o.cfgvalue = (sid, val) => {
			const res = view.map.data.data[sid].memory_reservation;
			return res ? '%1024.2m'.format(res) : 0;
		};
		o.write = function(section_id, value) {
			if (!value || value == 0) return 0;
			this.map.data.data[section_id].memory_reservation = view.parseMemory(value);;
			return view.parseMemory(value);
		};

		o = s.option(form.Value, 'blkio_weight', _('Block IO Weight'),
			_('Block IO weight (relative weight) accepts a weight value between 10 and 1000.'));
		o.rmempty = true;
		o.placeholder='500';
		o.datatype = 'and(uinteger,min(10),max(1000))';
		o.depends('advanced', 1);

		o = s.option(form.DynamicList, 'log_opt', _('Log driver options'),
			_('The logging configuration for this container'));
		o.rmempty = true;
		o.placeholder='max-size=1m';
		o.depends('advanced', 1);

		o = s.option(form.ListValue, 'log_driver', _('Log driver'));
		o.rmempty = true;
		o.value('', _('Default (daemon)'));
		o.value('local', _('local'));
		o.value('json-file', _('json-file'));
		o.value('syslog', _('syslog'));
		o.value('none', _('none'));
		o.depends('advanced', 1);

		this.map = m;

		return m.render();

	},

	handleSave(ev) {
		ev?.preventDefault();
		const view = this; // Capture the view context
		const map = this.map;
		if (!map)
			return Promise.reject(new Error(_('Form is not ready yet.')));

		const listToKv = view.listToKv;

		const toBool = (val) => (val === 1 || val === '1' || val === true);
		const toInt = (val) => val ? Number.parseInt(val) : undefined;
		const toFloat = (val) => val ? Number.parseFloat(val) : undefined;

		return map.parse()
			.then(() => {
				const get = (opt) => map.data.get('json', 'container', opt);
				const name = get('name');
				const log_driver = get('log_driver');
				// const pull = toBool(get('pull'));
				const network = get('network');
				const network_aliases = get('network_aliases');
				const publish = get('publish');
				const command = get('command');
				// const publish_all = toBool(get('publish_all'));
				const device = get('device');
				const tmpfs = get('tmpfs');
				const sysctl = get('sysctl');
				const log_opt = get('log_opt');

				const createBody = {
					Hostname: get('hostname'),
					User: get('user'),
					AttachStdin: toBool(get('interactive')),
					Tty: toBool(get('tty')),
					OpenStdin: toBool(get('interactive')),
					Env: get('env'),
					Cmd: command ? (command.match(/(?:[^\s"]+|"[^"]*")+/g) || []).map(arg => arg.replace(/^"|"$/g, '')) : null,
					Image: get('image'),
					HostConfig: {
						CpuShares: toInt(get('cpu_shares')),
						Memory: toInt(get('memory')),
						MemoryReservation: toInt(get('memory_reservation')),
						BlkioWeight: toInt(get('blkio_weight')),
						CapAdd: get('cap_add'),
						CpuPeriod: toInt(get('cpu_period')),
						CpuQuota: toInt(get('cpu_quota')),
						NanoCPUs: toFloat(get('cpus')) * (10 ** 9),
						Devices: device ? device
							.filter(d => d && typeof d === 'string' && d.trim().length > 0)
							.map(d => {
							const parts = d.split(':');
							return {
								PathOnHost: parts[0],
								PathInContainer: parts[1] || parts[0],
								CgroupPermissions: parts[2] || 'rwm'
							};
						}) : undefined,
						LogConfig: log_driver ? {
							Type: log_driver,
							Config: listToKv(log_opt)
						} : (log_opt ? {
							Type: 'json-file',
							Config: listToKv(log_opt)
						} : undefined),
						NetworkMode: network,
						PortBindings: publish ? Object.fromEntries(
							(Array.isArray(publish) ? publish : [publish])
							.filter(p => p && typeof p === 'string' && p.trim().length > 0)
							.map(p => {
								// hostIp:hostPort:cPort/proto (e.g. 192.168.1.100:8080:80/tcp)
								const m = p.match(/^([^:]+):(\d+):(\d+)\/(tcp|udp)$/);
								if (m) {
									const hostIp   = m[1];
									const hostPort = m[2];
									const cPort    = m[3];
									const proto    = m[4];
									return [`${cPort}/${proto}`, [{ HostIp: hostIp, HostPort: hostPort }]];
								}
								// [ipv6]:hostPort:cPort/proto (e.g. [::1]:8080:80/tcp)
								const m6 = p.match(/^\[([^\]]+)\]:(\d+):(\d+)\/(tcp|udp)$/);
								if (m6) {
									const hostIp   = m6[1];
									const hostPort = m6[2];
									const cPort    = m6[3];
									const proto    = m6[4];
									return [`${cPort}/${proto}`, [{ HostIp: hostIp, HostPort: hostPort }]];
								}
								// hostPort:cPort/proto (e.g. 8080:80/tcp)
								const m2 = p.match(/^(\d+):(\d+)\/(tcp|udp)$/);
								if (m2) {
									const hostPort = m2[1];
									const cPort    = m2[2];
									const proto    = m2[3];
									return [`${cPort}/${proto}`, [{ HostPort: hostPort }]];
								}
								return null;
							}).filter(Boolean)
						) : undefined,
						Mounts: undefined,
						Links: get('link'),
						Privileged: toBool(get('privileged')),
						PublishAllPorts: toBool(get('publish_all')),
						RestartPolicy: { Name: get('restart_policy') },
						Dns: get('dns'),
						Tmpfs: tmpfs ? Object.fromEntries(
							(Array.isArray(tmpfs) ? tmpfs : [tmpfs])
							.filter(t => t && typeof t === 'string' && t.trim().length > 0)
							.map(t => {
								const parts = t.split(':');
								return [parts[0], parts[1] || ''];
							})
						) : undefined,
						Sysctls: sysctl ? listToKv(sysctl) : undefined,
					},
					NetworkingConfig: {
						EndpointsConfig: { [network]: { 
							IPAMConfig: { IPv4Address: get('ipv4') || null, IPv6Address: get('ipv6') || null },
							Aliases: network_aliases ? network_aliases.split(',').map(a => a.trim()).filter(Boolean) : null
						} },
					}
				};

				// Parse volume entries and populate Mounts
				const volumeEntries = get('volume') || [];
				const volumeNames = new Set((view.volumes || []).map(v => v.Name));
				const volumeIds = new Set((view.volumes || []).map(v => v.Id));
				const mounts = [];
				for (const entry of volumeEntries) {
					let e = typeof entry === 'string' ? entry : '';
					let f = e.split(':')?.map(e => e && e.trim() || '');
					let source = f[0];
					let target = f[1];
					let options = f[2];

					if (!options) options = '';

					// Validate source and target are not empty
					if (!source || !target) {
						console.warn('Invalid volume entry (empty source or target):', entry);
						continue;
					}

					// Infer type: '@image' => image; '@tmpfs' => tmpfs; volume by name/id; else bind
					let type = 'bind';
					if (source === '@image') {
						type = 'image';
					} else if (source === '@tmpfs') {
						type = 'tmpfs';
					} else if (volumeNames.has(source) || volumeIds.has(source)) {
						type = 'volume';
					}

					const mount = {
						Type: type,
						Source: source,
						Target: target,
						ReadOnly: options.split(',').includes('ro')
					};

					// Add type-specific options
					if (type === 'bind') {
						const bindOptions = {};
						const propagation = options.split(',').find(opt => 
							['rprivate', 'private', 'rshared', 'shared', 'rslave', 'slave'].includes(opt)
						);
						if (propagation) bindOptions.Propagation = propagation;
						if (Object.keys(bindOptions).length > 0) mount.BindOptions = bindOptions;
					} else if (type === 'volume') {
						const volumeOptions = {};
						if (options.includes('nocopy')) volumeOptions.NoCopy = true;
						if (Object.keys(volumeOptions).length > 0) mount.VolumeOptions = volumeOptions;
					} else if (type === 'image') {
						const imageOptions = {};
						const subpathOpt = options.split(',').find(opt => opt.startsWith('subpath='));
						if (subpathOpt) imageOptions.Subpath = subpathOpt.slice('subpath='.length);
						if (Object.keys(imageOptions).length > 0) mount.ImageOptions = imageOptions;
						// Image source is implied by selected container image
						mount.Source = createBody.Image;
					} else if (type === 'tmpfs') {
						const tmpfsOptions = {};
						const optsList = options.split(',').map(o => o.trim()).filter(Boolean);
						for (const opt of optsList) {
							if (opt.startsWith('size=')) tmpfsOptions.SizeBytes = toInt(opt.slice('size='.length));
							else if (opt.startsWith('mode=')) tmpfsOptions.Mode = toInt(opt.slice('mode='.length));
							else {
								if (!tmpfsOptions.Options) tmpfsOptions.Options = [];
								const kv = opt.split('=');
								if (kv.length === 2) tmpfsOptions.Options.push([kv[0], kv[1]]);
								else if (kv.length === 1) tmpfsOptions.Options.push([kv[0]]);
							}
						}
						mount.Source = '';
						if (Object.keys(tmpfsOptions).length > 0) mount.TmpfsOptions = tmpfsOptions;
					}

					mounts.push(mount);
				}
				createBody.HostConfig.Mounts = mounts.length > 0 ? mounts : undefined;

				// Clean up undefined values
				Object.keys(createBody.HostConfig).forEach(key => {
					if (createBody.HostConfig[key] === undefined)
						delete createBody.HostConfig[key];
				});

				if (!name)
					return Promise.reject(new Error(_('No name specified.')));

				return { name, createBody };
			})
			.then(({ name, createBody }) => view.executeDockerAction(
				dm2.container_create,
				{ query: { name: name }, body: createBody },
				_('Create container'),
				{
					showOutput: false,
					showSuccess: false,
					onSuccess: (response) => {
						const isDuplicate = view.isDuplicate && view.duplicateContainer;
						const msgTitle = isDuplicate ? _('Container duplicated') : _('Container created');
						const msgText = isDuplicate ? 
							_('New container duplicated from ') + view.duplicateContainer.Name?.substring(1) : 
							_('New container has been created.');

						if (response?.body?.Warnings) {
							view.showNotification(msgTitle + _(' with warnings'), response?.body?.Warning || msgText, 5000, 'warning');
						} else {
							view.showNotification(msgTitle, msgText, 4000, 'success');
						}
						window.location.href = `${this.dockerman_url}/containers`;
					}
				}
			))
			.catch((err) => {
				view.showNotification(_('Create container failed'), err?.message || String(err), 7000, 'error');
				return false;
			});
	},

	handleSaveApply: null,
	handleReset: null,

});
