'use strict';
'require form';
'require fs';
'require poll';
'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:

GET /containers/{id}/json: the NetworkSettings no longer returns the deprecated
 Bridge, HairpinMode, LinkLocalIPv6Address, LinkLocalIPv6PrefixLen,
 SecondaryIPAddresses, SecondaryIPv6Addresses, EndpointID, Gateway,
 GlobalIPv6Address, GlobalIPv6PrefixLen, IPAddress, IPPrefixLen, IPv6Gateway,
 and MacAddress fields. These fields were deprecated in API v1.21 (docker
 v1.9.0) but kept around for backward compatibility.

*/

return dm2.dv.extend({
	load() {
		return Promise.all([
			dm2.container_list({query: {all: true}}),
			dm2.image_list({query: {all: true}}),
			dm2.network_list({query: {all: true}}),
		]);
	},

	render([containers, images, networks]) {
		if (containers?.code !== 200) {
			return E('div', {}, [ containers?.body?.message ]);
		}

		let container_list = containers.body;
		let network_list = networks.body;
		let image_list = images.body;

		const view = this;
		let containerTable;


		const m = new form.JSONMap({container: view.getContainersTable(container_list, image_list, network_list), prune: {}},
			_('Docker - Containers'),
			_('This page displays all docker Containers that have been created on the connected docker host.') + '<br />' +
			_('Note: docker provides no container import facility.'));
		m.submit = false;
		m.reset = false;

		let s, o;


		let pollPending = null;
		let conSec = null;
		const calculateTotals = () => {
			return {
				running_total: Array.isArray(container_list) ?
					container_list.filter(c => c?.State === 'running').length : 0,
				paused_total: Array.isArray(container_list) ?
					container_list.filter(c => c?.State === 'paused').length : 0,
				stopped_total: Array.isArray(container_list) ?
					container_list.filter(c => ['exited', 'created'].includes(c?.State)).length : 0
			};
		};

		const refresh = () => {
			if (pollPending) return pollPending;
			pollPending = view.load().then(([containers2, images2, networks2]) => {
				image_list = images2.body;
				container_list = containers2.body;
				network_list = networks2.body;
				m.data = new m.data.constructor({ container: view.getContainersTable(container_list, image_list, network_list), prune: {} });

				const totals = calculateTotals();
				if (conSec) {
					conSec.footer = [
						`${_('Total')} ${container_list.length}`,
						[
							`${_('Running')} ${totals.running_total}`,
							E('br'),
							`${_('Paused')} ${totals.paused_total}`,
							E('br'),
							`${_('Stopped')} ${totals.stopped_total}`,
						],
						'',
						'',
					];
				}
				
				return m.render();
			}).catch((err) => { console.warn(err) }).finally(() => { pollPending = null });
			return pollPending;
		};

		s = m.section(form.TableSection, 'prune', _('Containers overview'), null);
		s.addremove = false;
		s.anonymous = true;

		const prune = s.option(form.Button, '_prune', null);
		prune.inputtitle = `${dm2.ActionTypes['prune'].i18n} ${dm2.ActionTypes['prune'].e}`;
		prune.inputstyle = 'negative';
		prune.onclick = L.bind(function(section_id, ev) {
			return this.super('handleXHRTransfer', [{
				q_params: {  },
				commandCPath: '/containers/prune',
				commandDPath: '/containers/prune',
				commandTitle: dm2.ActionTypes['prune'].i18n,
				onUpdate: (msg) => {
					try {
						if(msg.error)
							ui.addTimeLimitedNotification(dm2.ActionTypes['prune'].i18n, msg.error, 7000, 'error');

						const output = JSON.stringify(msg, null, 2) + '\n';
						view.insertOutput(output);
					} catch { }
				},
				noFileUpload: true,
			}]);
		}, this);

		const totals = calculateTotals();
		let running_total = totals.running_total;
		let paused_total = totals.paused_total;
		let stopped_total = totals.stopped_total;

		conSec = m.section(form.TableSection, 'container');
		conSec.anonymous = true;
		conSec.nodescriptions = true;
		conSec.addremove = true;
		conSec.sortable = true;
		conSec.filterrow = true;
		conSec.addbtntitle = `${dm2.ActionTypes['create'].i18n} ${dm2.ActionTypes['create'].e}`;
		conSec.footer = [
			`${_('Total')} ${container_list.length}`,
			[
				`${_('Running')} ${running_total}`,
				E('br'),
				`${_('Paused')} ${paused_total}`,
				E('br'),
				`${_('Stopped')} ${stopped_total}`,
			],
			'',
			'',
		];

		conSec.handleAdd = function(section_id, ev) {
			window.location.href = `${view.dockerman_url}/container_new`;
		};

		conSec.renderRowActions = function(sid) {
			const cont = this.map.data.data[sid];
			return view.buildContainerActions(cont);
		}

		o = conSec.option(form.DummyValue, 'cid', _('Container'));
		o = conSec.option(form.DummyValue, 'State', _('State'));
		o = conSec.option(form.DummyValue, 'Networks', _('Networks'));
		o.rawhtml = true;
		o = conSec.option(form.DummyValue, 'Ports', _('Ports'));
		o.rawhtml = true;
		o = conSec.option(form.DummyValue, 'Command', _('Command'));
		o.width = 200;
		o = conSec.option(form.DummyValue, 'Created', _('Created'));

		poll.add(L.bind(() => { refresh(); }, this), 10);

		this.insertOutputFrame(conSec, m);
		return m.render();

	},

	buildContainerActions(cont, idx) {
		const view = this;
		const isRunning = cont?.State === 'running';
		const isRestarting = cont?.State === 'restarting';
		const isPaused = cont?.State === 'paused';
		const btns = [
			E('button', {
				'class': 'cbi-button view',
				'title': dm2.ActionTypes['inspect'].i18n,
				'click': () => view.executeDockerAction(
					dm2.container_inspect,
					{id: cont.Id},
					dm2.ActionTypes['inspect'].i18n,
					{showOutput: true, showSuccess: false}
				)
			}, [dm2.ActionTypes['inspect'].e]),

			E('button', {
				'class': 'cbi-button cbi-button-positive edit',
				'title': _('Edit this container'),
				'click': () => window.location.href = `${view.dockerman_url}/container/${cont?.Id}`
			}, [dm2.ActionTypes['edit'].e]),

			(() => {
				const icon = isRunning
					? dm2.Types['container'].sub['pause'].e
					: (isPaused 
						? dm2.Types['container'].sub['unpause'].e
						: dm2.Types['container'].sub['start'].e);
				const title = isRunning
					? _('Pause this container')
					: (isPaused ? _('Unpause this container') : _('Start this container'));
				const handler = isRunning
					? () => view.executeDockerAction(
							dm2.container_pause,
							{id: cont.Id},
							dm2.Types['container'].sub['pause'].i18n,
							{showOutput: true, showSuccess: false}
						)
					: (isPaused ? () => view.executeDockerAction(
							dm2.container_unpause,
							{id: cont.Id},
							dm2.Types['container'].sub['unpause'].i18n,
							{showOutput: true, showSuccess: false}
						) : () => view.executeDockerAction(
							dm2.container_start,
							{id: cont.Id},
							dm2.Types['container'].sub['start'].i18n,
							{showOutput: true, showSuccess: false}
						));
				const btnClass = isRunning ? 'cbi-button cbi-button-neutral' : 'cbi-button cbi-button-positive start';

				return E('button', {
					'class': btnClass,
					'title': title,
					'click': handler,
				}, [icon]);
			})(),

			E('button', {
				'class': 'cbi-button cbi-button-neutral restart',
				'title': _('Restart this container'),
				'click': () => view.executeDockerAction(
					dm2.container_restart,
					{id: cont.Id},
					_('Restart'),
					{showOutput: true, showSuccess: false}
				)
			}, [dm2.Types['container'].sub['restart'].e]),

			E('button', {
				'class': 'cbi-button cbi-button-neutral stop',
				'title': _('Stop this container'),
				'click': () => view.executeDockerAction(
					dm2.container_stop,
					{id: cont.Id},
					dm2.Types['container'].sub['stop'].i18n,
					{showOutput: true, showSuccess: false}
				),
				'disabled' : !(isRunning || isPaused || isRestarting) ? true : null
			}, [dm2.Types['container'].sub['stop'].e]),

			E('button', {
				'class': 'cbi-button cbi-button-negative kill',
				'title': _('Kill this container'),
				'click': () => view.executeDockerAction(
					dm2.container_kill,
					{id: cont.Id},
					dm2.Types['container'].sub['kill'].i18n,
					{showOutput: true, showSuccess: false}
				),
				'disabled' : !(isRunning || isPaused || isRestarting) ? true : null
			}, [dm2.Types['container'].sub['kill'].e]),

			E('button', {
				'class': 'cbi-button cbi-button-neutral export',
				'title': _('Export this container'),
				'click': () => {
					window.location.href = `${view.dockerman_url}/container/export/${cont.Id}`;
				}
			}, [dm2.Types['container'].sub['export'].e]),

			E('div', {
				'style': 'width: 20px',
				// Some safety margin for mis-clicks
			}, [' ']),

			E('button', {
				'class': 'cbi-button cbi-button-negative remove',
				'title': dm2.ActionTypes['remove'].i18n,
				'click': () => view.executeDockerAction(
					dm2.container_remove,
					{id: cont.Id, query: { force: false }},
					dm2.ActionTypes['remove'].i18n,
					{showOutput: true, showSuccess: false}
				)
			}, [dm2.ActionTypes['remove'].e]),

			E('button', {
				'class': 'cbi-button cbi-button-negative important remove',
				'title': dm2.ActionTypes['force_remove'].i18n,
				'click': () => view.executeDockerAction(
					dm2.container_remove,
					{id: cont.Id, query: { force: true }},
					_('Force Remove'),
					{showOutput: true, showSuccess: false}
				)
			}, [dm2.ActionTypes['force_remove'].e]),
		];

		return E('td', { 
			'class': 'td',
		}, E('div', btns));
	},

	buildPortLinks(ports) {
		// cont.Ports[] from GET /containers/json — flat array.
		// Published ports have {IP, PrivatePort, PublicPort, Type}.
		// Exposed-only ports omit IP and PublicPort: {PrivatePort, Type}.
		if (!Array.isArray(ports) || ports.length === 0) return '';

		const LOCAL_IPS = new Set(['0.0.0.0', '::']);

		// Sort: published (has PublicPort) before exposed-only, then by PrivatePort
		const sorted = [...ports].sort((a, b) => {
			const aHasPub = a.PublicPort ? 1 : 0;
			const bHasPub = b.PublicPort ? 1 : 0;
			if (aHasPub !== bHasPub) return bHasPub - aHasPub;
			return (a.PrivatePort || 0) - (b.PrivatePort || 0);
		});

		const lines = sorted.map(p => {
			const ip   = p.IP || '';
			const pub  = p.PublicPort || '';
			const priv = p.PrivatePort || '';
			const type = p.Type || '';

			const isIPv6    = ip.includes(':');
			const isLocal   = LOCAL_IPS.has(ip);
			const displayIp = isIPv6 ? `[${ip}]` : ip;

			let label;
			if (pub && ip)  label = `${displayIp}:${pub}->${priv}/${type}`;
			else if (pub)   label = `${pub}->${priv}/${type}`;
			else            label = `${priv}/${type}`;

			// Clickable link for published TCP ports only
			if (type === 'tcp' && pub) {
				const host = isLocal ? window.location.hostname : displayIp;
				return E('div', {}, [
					E('a', {
						href: `http://${host}:${pub}`,
						target: '_blank',
						rel: 'noopener noreferrer',
						title: _('Open in browser'),
					}, [label]),
				]);
			}

			return E('div', {}, [label]);
		});

		return E('div', {}, lines);
	},

	handleSave: null,
	handleSaveApply: null,
	handleReset: null,

	getContainersTable(containers, image_list, network_list) {
		const data = [];

		for (const cont of Array.isArray(containers) ? containers : []) {

			// build Container ID: xxxxxxx image: xxxx
			const names = Array.isArray(cont?.Names) ? cont.Names : [];
			const cleanedNames = names
				.map(n => (typeof n === 'string' ? n.substring(1) : ''))
				.filter(Boolean)
				.join(', ');
			const statusColorName = this.wrapStatusText(cleanedNames, cont.State, 'font-weight:600;');
			const imageName = this.getImageFirstTag(image_list, cont.ImageID);
			const shortId = (cont?.Id || '').substring(0, 12);

			const cid = E('div', {}, [
					E('a', { href: `container/${cont.Id}`, title: dm2.ActionTypes['edit'].i18n }, [
						statusColorName,
						E('div', { 'style': 'font-size: 0.9em; font-family: monospace; ' }, [`ID: ${shortId}`]),
					]),
				E('div', { 'style': 'font-size: 0.85em;' }, [`${dm2.Types['image'].i18n}: ${imageName}`]),
			])

			// Just push plain data objects without UCI metadata
			data.push({
				...cont,
				cid: cid,
				_shortId: (cont?.Id || '').substring(0, 12),
				Networks: this.parseNetworkLinksForContainer(network_list, cont?.NetworkSettings?.Networks || {}, true),
				Created: this.buildTimeString(cont?.Created) || '',
				Ports: this.buildPortLinks(cont.Ports),
			});
		}

		return data;
	},

});
