// form/nc/nc-admin/nc-nats/nc-nats.js
// NATS Testing, Statistics, and Controlling Admin Page

let nats
const change = nc.change
let gridSubscription = null
let changeSubscription = null
let statsUpdateInterval = null
let currentBucket = ''
let natsConnection = null
let subscribed = false

let changeAddCount = 0
let changeModifyCount = 0
let changeDeleteCount = 0
let changeModifySkipCount = 0
let changeDeleteSkipCount = 0
let msgAddCount = 0
let msgModifyCount = 0
let msgDeleteCount = 0

function time(startTime) {
	return startTime > 0 ? `${((performance.now() - startTime) / 1000).toFixed(3)} sec` : '0.000 sec'
}

function getTestBucket() {
	return `${rec.bucket}-${rec.row_count}`
}

function closeSubscription(sub, label) {
	if (!sub) {
		return
	}
	const start = performance.now()
	try {
		if (sub.unsubscribe) {
			sub.unsubscribe()
		} else if (sub.close) {
			sub.close()
		}
		console.log(`NATS: '${currentBucket}' unsubscribed ${label} ${((performance.now() - start) / 1000).toFixed(3)}s`)
	} catch (err) {
		console.error(`NATS: '${currentBucket}' error unsubscribing ${label}:`, err)
	}
}

function unsubscribeAll() {
	closeSubscription(gridSubscription, 'grid')
	closeSubscription(changeSubscription, 'change')
	gridSubscription = null
	changeSubscription = null
}

function resetChangeCounters() {
	changeAddCount = 0
	changeModifyCount = 0
	changeDeleteCount = 0
	changeModifySkipCount = 0
	changeDeleteSkipCount = 0
	msgAddCount = 0
	msgModifyCount = 0
	msgDeleteCount = 0
}

onMounted(async () => {
	initCompressionDefaults()
	await initConnection()
	await selectFirstConnection()
	updateStatLabels()
})

onUnmounted(() => {
	if (statsUpdateInterval) {
		clearInterval(statsUpdateInterval)
	}
	unsubscribeAll()
})

function updateStatLabels() {
	if (nats?.messageTracker == null) {
		return
	}
	// Message and row count display fields
	rec.received_message_label = `${nc.formatNumber(rec.received_message_total)} messages`
	rec.received_row_label = `${nc.formatNumber(rec.received_row_total)} rows`
	// Rate fields
	rec.progress = rec.total_row_count > 0 ? (rec.grid_row_count / rec.total_row_count) * 100 : null
	rec.message_rate = `${calculateMessageRate()} msgs / sec`
	rec.row_rate = `${calculateRowRate()} rows / sec`
	rec.total_row_count = nats.messageTracker.total_row_count
	const totalTimeMs = rec.end_time > 0 ? rec.end_time - rec.start_time : rec.start_time > 0 ? performance.now() - rec.start_time : 0
	rec.total_time = totalTimeMs > 0 ? `${(totalTimeMs / 1000).toFixed(3)} sec` : ''
	const time = rec.received_message_total > 0 ? (totalTimeMs / rec.received_message_total).toFixed(1) : 0
	rec.avg_time = time > 0 ? `${time} ms / msg` : ''
	// Compression display fields - read from messageTracker
	if (nats.messageTracker.compression_level != null) {
		rec.compression_level = `Level ${nats.messageTracker.compression_level}`
	}
	if (nats.messageTracker.compression_time > 0) {
		rec.compression_time = `${nats.messageTracker.compression_time.toFixed(4)} sec`
	}
	if (nats.messageTracker.decompression_time > 0) {
		rec.decompression_time = `${nats.messageTracker.decompression_time.toFixed(4)} sec`
	}
	if (nats.messageTracker.original_bytes > 0) {
		rec.original_bytes = formatKilobytes(nats.messageTracker.original_bytes)
	}
	if (nats.messageTracker.compressed_bytes > 0) {
		rec.compressed_bytes = formatKilobytes(nats.messageTracker.compressed_bytes)
	}
	if (nats.messageTracker.original_bytes > 0 && nats.messageTracker.compressed_bytes > 0) {
		rec.compression_percent = (((nats.messageTracker.original_bytes - nats.messageTracker.compressed_bytes) / nats.messageTracker.original_bytes) * 100).toFixed(1) + ' %'
	}
	if (rec.progress <= 30) {
		rec.progress_status = 'error'
	} else if (rec.progress <= 90) {
		rec.progress_status = 'warning'
	} else {
		rec.progress_status = 'success'
	}
	// Progress message
	rec.message_text = `${nc.formatNumber(rec.grid_row_count)} / ${nc.formatNumber(rec.total_row_count)} rows (${rec.total_time}, ${nc.formatNumber(rec.received_message_total)} messages, ${rec.message_rate}, ${rec.row_rate})`
}

function initCompressionDefaults() {
	rec.compression_level = 'Off'
	rec.compression_time = ''
	rec.decompression_time = ''
	rec.original_bytes = ''
	rec.compressed_bytes = ''
	rec.compression_percent = ''
}

async function initConnection() {
	nats = await nc.getNats()
	const hostname = window.location.hostname
	const isHttps = window.location.protocol === 'https:'
	let testUrlArray
	if (hostname === 'localhost') {
		testUrlArray = isHttps ? [`wss://localhost:4223`] : [`ws://localhost:4223`]
	} else {
		// `wss://capacic.com/ws`, `wss://capacic.dev/ws`
		testUrlArray = isHttps ? [`wss://${hostname}/ws`, `ws://localhost:4223`] : [`ws://${hostname}/ws`, `ws://localhost:4223`]
	}
	for (const url of testUrlArray) {
		// Add all URLs to connection array first
		const exists = arr.connection.some(c => c.value === url)
		if (!exists) {
			arr.connection.push({ value: url, show: url })
		}
	}
	// Find first working connection
	for (const url of testUrlArray) {
		try {
			const testConnection = await nats.connectNats(state, {
				server: url,
				test_only: true
			})
			if (testConnection) {
				// Move working URL to front
				const idx = arr.connection.findIndex(c => c.value === url)
				if (idx > 0) {
					const item = arr.connection.splice(idx, 1)[0]
					arr.connection.unshift(item)
				}
				await testConnection.close()
				break
			}
		} catch {
			// Continue to next URL
		}
	}
}

async function selectFirstConnection() {
	if (arr.connection.length > 0 && !rec.connection) {
		rec.connection = arr.connection[0].value
		await connectionChanged()
	}
}

async function connectionChanged() {
	if (!rec.connection) {
		return
	}
	const oldConnection = natsConnection
	natsConnection = null
	rec.connected = false
	rec.connection_info = 'Connecting...'
	updateStatLabels()
	try {
		natsConnection = await nats.connectNats(state, {
			server: rec.connection,
			force: true,
			user: rec.nats_user,
			pass: rec.nats_pass
		})
		if (natsConnection) {
			rec.connected = true
			rec.connection_info = natsConnection.getServer()
		} else {
			rec.connection_info = 'Connection failed'
		}
	} catch (err) {
		rec.connection_info = 'Connection error: ' + (err?.message || 'unknown')
		console.error('NATS: connection error:', err)
	}
	rec.connected = !!natsConnection
	updateStatLabels()
	if (oldConnection && oldConnection !== natsConnection) {
		try {
			await oldConnection.close()
		} catch {
			// Ignore close errors
		}
	}
}

async function subscribeToNatsBucket(natsConnection, bucket) {
	if (!natsConnection) {
		message.setError(state, 'NATS: cannot subscribe - no connection')
		return
	}
	if (!bucket) {
		message.setError(state, 'NATS: cannot subscribe - no bucket specified')
		return
	}
	console.log(`NATS: '${bucket}' subscribing`)
	const gridStart = performance.now()
	let gridSub = null
	try {
		gridSub = await nats.subscribeToGrid(state, natsConnection, bucket, statMessage)
		const gridTime = ((performance.now() - gridStart) / 1000).toFixed(3)
		const changeStart = performance.now()
		changeSubscription = await nats.subscribeToChange(state, natsConnection, bucket, changeMessage)
		const changeTime = ((performance.now() - changeStart) / 1000).toFixed(3)
		gridSubscription = gridSub
		console.log(`NATS: '${bucket}' subscribed [grid: ${gridTime}s, change: ${changeTime}s] ${time(rec.start_time)}`)
	} catch (err) {
		closeSubscription(gridSub, 'grid (cleanup)')
		gridSubscription = null
		changeSubscription = null
		throw err
	}
}

function statMessage(message, stat) {
	if (!message) {
		return
	}
	rec.received_message_total += 1
	rec.grid_row_count = state.grid.message?.data?.length || 0
	rec.received_row_total = rec.grid_row_count
	if (rec.running && !rec.current_message_id) {
		rec.current_message_id = message.record_id
		console.log(`NATS: '${message.bucket}' record_id ${message.record_id} ${time(rec.start_time)}`)
	}
	const statArray = state.grid.message_stat.data
	const statRow = {
		idx: statArray.length + 1,
		record_id: message.record_id,
		msg_index: message.msg_index,
		message_count: nc.formatNumber(message.message_count),
		row_count: nc.formatNumber(stat.row_count),
		bucket: message.bucket,
		is_complete: message.is_complete ? 'Yes' : 'No',
		timestamp: message.timestamp,
		process_time: stat.process_time
	}
	statArray.push(statRow)
	if (rec.running && message.is_complete) {
		const trackerCount = Object.keys(nats.messageTracker.idx).length
		if (trackerCount === 0) {
			console.log(`NATS: '${message.bucket}' complete, rows: ${nc.formatNumber(rec.grid_row_count)} ${time(rec.start_time)}`)
			rec.running = false
			rec.end_time = performance.now()
		}
	}
	if (stat.redraw) {
		updateStatLabels()
		if (stat.redraw_grid || statArray.length <= 100) {
			nc.grid.redrawRows(state, 'message_stat')
		}
	}
	if (rec.first_message_time == null) {
		rec.first_message_time = time(rec.start_time)
	}
}

function updateChangeLabel() {
	const gridRowCount = state.grid?.message?.data?.length || 0
	const receivedTotal = msgAddCount + msgModifyCount + msgDeleteCount
	const appliedTotal = changeAddCount + changeModifyCount + changeDeleteCount
	const partArray = []
	partArray[partArray.length] = `${nc.formatNumber(rec.received_change_total)} message${rec.received_change_total !== 1 ? 's' : ''}`
	const detailPartArray = []
	if (changeDeleteCount > 0) {
		detailPartArray[detailPartArray.length] = `${changeDeleteCount}/${msgDeleteCount} deleted`
	}
	if (changeModifyCount > 0) {
		detailPartArray[detailPartArray.length] = `${changeModifyCount}/${msgModifyCount} changed`
	}
	if (changeAddCount > 0) {
		detailPartArray[detailPartArray.length] = `${changeAddCount}/${msgAddCount} added`
	}
	if (changeModifySkipCount > 0 || changeDeleteSkipCount > 0) {
		const skipPartArray = []
		if (changeDeleteSkipCount > 0) {
			skipPartArray[skipPartArray.length] = `${changeDeleteSkipCount} not found`
		}
		if (changeModifySkipCount > 0) {
			skipPartArray[skipPartArray.length] = `${changeModifySkipCount} already deleted`
		}
		detailPartArray[detailPartArray.length] = skipPartArray.join(', ') + ' skipped'
	}
	if (detailPartArray.length > 0) {
		partArray[partArray.length] = `applied ${appliedTotal}/${receivedTotal}: ${detailPartArray.join(', ')}`
	}
	partArray[partArray.length] = `grid: ${nc.formatNumber(gridRowCount)} rows`
	rec.received_change_label = partArray.join(' | ')
}

function accumulateFlushResult(flushResult) {
	changeAddCount += flushResult.add
	changeModifyCount += flushResult.modify
	changeDeleteCount += flushResult.delete
	changeModifySkipCount += flushResult.modify_skip
	changeDeleteSkipCount += flushResult.delete_skip
}

function changeMessage(changeData) {
	if (!changeData) {
		return
	}
	if (changeData.data) {
		return
	}
	if (!changeData.change_idx) {
		console.log(`NATS: '${currentBucket}' change missing change_idx`)
		return
	}
	rec.received_change_total += 1
	for (const tableName in changeData.change_idx) {
		const tableChange = changeData.change_idx[tableName]
		if (tableChange && typeof tableChange === 'object') {
			msgAddCount += tableChange.add_count
			msgModifyCount += tableChange.modify_count
			msgDeleteCount += tableChange.delete_count
			if (tableChange.add_count + tableChange.modify_count > 0) {
				console.log(`NATS: '${currentBucket}' table ${tableName}: add=${tableChange.add_count} mod=${tableChange.modify_count} del=${tableChange.delete_count}`)
			}
		}
	}
	change.mergeChangeArr(state, changeData.change_idx, result => {
		if (result) {
			accumulateFlushResult(result)
		}
		updateChangeLabel()
		nc.grid.redrawRows(state, 'message')
	})
}

// eslint-disable-next-line no-unused-vars -- Called from layout JSON
function calculateBytes(data) {
	let total = 0
	for (const row of data) {
		total += JSON.stringify(row).length
	}
	return total
}

function formatKilobytes(byteCount) {
	const kilobytes = (Number(byteCount) || 0) / 1024
	return `${nc.formatNumber(kilobytes.toFixed(2))} KB`
}

function calculateMessageRate() {
	const totalTime = (performance.now() - rec.start_time) / 1000
	return totalTime > 0 ? nc.formatNumber(Math.round(rec.received_message_total / totalTime)) : 0
}

function calculateRowRate() {
	const totalTime = (performance.now() - rec.start_time) / 1000
	return totalTime > 0 ? nc.formatNumber(Math.round(rec.grid_row_count / totalTime)) : 0
}

// eslint-disable-next-line no-unused-vars -- Called from layout JSON
async function startTest() {
	if (rec.running) {
		return
	}
	currentBucket = getTestBucket()
	unsubscribeAll()
	subscribed = false
	rec.current_message_id = ''
	rec.first_message_time = null
	rec.running = true
	rec.start_time = performance.now()
	rec.end_time = 0
	rec.total_row_count = 0
	rec.progress = 0
	rec.received_message_total = 0
	rec.received_change_total = 0
	rec.received_change_label = ''
	resetChangeCounters()
	rec.message_rate = 0
	rec.row_rate = 0
	rec.grid_changed_row = ''
	state.rec.change_state = {}
	state.grid.message.recordIdIdx = null
	nc.grid.dataChanged(state.grid.message, [])
	nc.grid.dataChanged(state.grid.message_stat, [])
	nats.resetMessageTracker()
	nc.callServer(
		state,
		'nats',
		{
			action: 'send-test',
			rec: {
				bucket: currentBucket,
				row_count: rec.row_count,
				change_message_count: rec.change_message_count,
				stream_prefix: 'GRID_',
				delete_all_prefix_streams: rec.delete_all_prefix_streams,
				delete_old_stream: rec.delete_old_stream
			}
		},
		testResponse
	)
	startStatsUpdate()
}

function testResponse(response) {
	if (response.error) {
		message.setError(state, `NATS test error: ${response.error.description || response.error.message || 'Unknown error'}`)
		rec.running = false
		return
	}
	if (nats.isSubscriptionStopped()) {
		return
	}
	if (!subscribed && natsConnection) {
		subscribed = true
		subscribeToNatsBucket(natsConnection, currentBucket).catch(err => {
			subscribed = false
			message.setError(state, `NATS: '${currentBucket}' subscribe failed, error: ${err?.message || 'unknown'}`)
			rec.running = false
		})
	}
}

// eslint-disable-next-line no-unused-vars -- Called from layout JSON
function stopTest() {
	if (!rec.running) {
		return
	}
	rec.running = false
	rec.end_time = Date.now()
	rec.message_rate = calculateMessageRate()
	stopStatsUpdate()
	nats.stopSubscription()
	if (!gridSubscription && !changeSubscription) {
		console.log(`NATS: '${currentBucket}' no subscription ${time(rec.start_time)}`)
	}
	unsubscribeAll()
}

// eslint-disable-next-line no-unused-vars -- Called from layout JSON
function clearGrid() {
	state.rec.change_state = {}
	state.grid.message.recordIdIdx = null
	nc.grid.dataChanged(state.grid.message, [])
	nc.grid.dataChanged(state.grid.message_stat, [])
	rec.grid_row_count = 0
	rec.progress = 0
	rec.total_row_count = 0
	rec.received_message_total = 0
	rec.received_change_total = 0
	rec.received_change_label = ''
	resetChangeCounters()
	rec.message_rate = 0
	rec.row_rate = 0
	rec.grid_changed_row = ''
}

function startStatsUpdate() {
	stopStatsUpdate()
	statsUpdateInterval = setInterval(() => {
		if (rec.running) {
			rec.message_rate = calculateMessageRate()
		}
	}, 1000)
}

function stopStatsUpdate() {
	if (statsUpdateInterval) {
		clearInterval(statsUpdateInterval)
		statsUpdateInterval = null
	}
}

// eslint-disable-next-line no-unused-vars -- Called from layout JSON
function gridRowDoubleClicked() {}

// eslint-disable-next-line no-unused-vars -- Called from layout JSON
function changeIconClicked(area, rowIndex) {
	const data = state.grid[area]?.data
	const row = data?.[rowIndex - 1]
	if (row?.record_id == null) {
		return
	}
	const changeState = state.rec.change_state?.[area]
	if (row.calc?.change === 'delete') {
		data.splice(rowIndex - 1, 1)
		if (changeState) {
			delete changeState.changeTypeIdx[row.record_id]
			delete changeState.changedFieldIdx[row.record_id]
			delete changeState.viewedIdx[row.record_id]
		}
		state.grid[area].recordIdIdx = null
		nc.grid.dataChanged(state.grid[area], data)
		return
	}
	if (changeState == null) {
		return
	}
	changeState.viewedIdx[row.record_id] = true
	nc.grid.redrawRows(state, area)
}

// eslint-disable-next-line no-unused-vars -- Called from layout JSON
function tabChanged() {
	console.log(`NATS: tab changed: ${rec.tab}`)
}
