// #region TranslationRunStore [C:4] [TYPE Store] [SEMANTICS translate, run, progress, websocket, store]
// @ingroup Stores
// @BRIEF Global store for active translation run progress — WebSocket-only real-time
//        streaming with reconnect + timeout safety nets. Survives page navigation AND
//        tab close/reopen via sessionStorage persistence.
// @LAYER UI
// @RELATION DEPENDS_ON -> [getTranslateRunWsUrl]
// @UX_STATE idle -> No active run
// @UX_STATE running -> WebSocket active, progress bar updates in real-time
// @UX_STATE completed/partial/failed/insert_failed/cancelled -> Terminal states
// @UX_FEEDBACK WebSocket streams batch completion + insert phase events
// @UX_RECOVERY WebSocket reconnect: up to 3 attempts with 10s backoff on disconnect
// @UX_RECOVERY Application timeout: 600s max — transitions to failed if stuck
// @UX_RECOVERY sessionStorage persistence: runId saved on start, cleared on terminal;
//            page reload or new tab picks it up via getStoredActiveRun() + reconnectToRun()
// @INVARIANT _state MUST be replaced (not mutated) on every update — `_state = {..._state, ...data}`.
//    $state in Svelte 5 tracks by reference. Mutating _state.xxx = y would bypass reactivity
//    because the reference doesn't change. New object ensures all $derived(translationRunStore.value)
//    re-evaluate correctly.
// @INVARIANT WS onmessage handler MUST call _notify() BEFORE _onCompleteCallback.
//    _notify() synchronously calls subscribers (TranslationRunGlobalIndicator, TranslationRunProgress)
//    which write to their local $state. These writes are deferred by Svelte's scheduler.
//    If _onCompleteCallback is called before _notify(), the callback's $state writes
//    (e.g. isRunning=false, runComplete=true) would be processed first, removing
//    TranslationRunProgress from the DOM before its effect can fire — silent bug.
// @RATIONALE Migrated from Svelte 4 writable+derived to Svelte 5 $state+$derived runes.
//            Polling fallback removed — WS-only with explicit reconnect + timeout.
//            sessionStorage added because in-memory store dies on page reload / new tab.
// @REJECTED mutation of _state properties (e.g. _state.uxState = 'running') — $state in
//    Svelte 5 tracks by reference. Property mutation doesn't change the reference, so
//    $derived values that read _state would not re-evaluate. Always spread: _state = {..._state, ...data}.

import { getTranslateRunWsUrl } from '$lib/api';
import { log } from '$lib/cot-logger';

export type UxState = 'idle' | 'running' | 'inserting' | 'completed' | 'partial' | 'failed' | 'insert_failed' | 'cancelled';

export interface TranslationRunState {
	runId: string | null;
	uxState: UxState;
	status: Record<string, unknown> | null;
	totalRecords: number;
	successfulRecords: number;
	failedRecords: number;
	skippedRecords: number;
	cacheHits: number;
	progressPct: number;
	insertStatus: string | null;
	batchCount: number;
	jobId: string | null;
	isFullRun: boolean;
}

export interface StartRunOptions {
	jobId?: string;
	isFullRun?: boolean;
	onComplete?: (_data: Record<string, unknown>) => void;
}

const initialState: TranslationRunState = {
	runId: null,
	uxState: 'idle',
	status: null,
	totalRecords: 0,
	successfulRecords: 0,
	failedRecords: 0,
	skippedRecords: 0,
	cacheHits: 0,
	progressPct: 0,
	insertStatus: null,
	batchCount: 0,
	jobId: null,
	isFullRun: false,
};

// Reactive state (Svelte 5 rune)
let _state: TranslationRunState = $state(initialState);

// Derived values (Svelte 5 rune)
let _isTranslationActive = $derived(_state.uxState === 'running' || _state.uxState === 'inserting');
let _isTranslationFinished = $derived(
	_state.uxState === 'completed' ||
	_state.uxState === 'partial' ||
	_state.uxState === 'failed' ||
	_state.uxState === 'insert_failed' ||
	_state.uxState === 'cancelled',
);

// Subscriber list for backward compat with $translationRunStore syntax
let _subscribers: Array<(_value: TranslationRunState) => void> = [];

function _notify(): void {
	const current = _state;
	_subscribers.forEach((fn) => fn(current));
}

export const translationRunStore = {
	subscribe(run: (_value: TranslationRunState) => void): () => void {
		_subscribers.push(run);
		run(_state);
		return () => {
			_subscribers = _subscribers.filter((fn) => fn !== run);
		};
	},
	get value(): TranslationRunState {
		return _state;
	},
	set(newState: TranslationRunState) {
		_state = newState;
		_notify();
	},
	update(fn: (_state: TranslationRunState) => TranslationRunState) {
		_state = fn(_state);
		_notify();
	},
	get isTranslationActive(): boolean {
		return _isTranslationActive;
	},
	get isTranslationFinished(): boolean {
		return _isTranslationFinished;
	},
};

let _onCompleteCallback: ((_data: Record<string, unknown>) => void) | null = null;
let _ws: WebSocket | null = null;
let _reconnectCount = 0;
let _reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let _timeoutTimer: ReturnType<typeof setTimeout> | null = null;
const MAX_RECONNECT = 3;
const RECONNECT_DELAY_MS = 10_000;
const RUN_TIMEOUT_MS = 600_000; // 10 minutes
const SESSION_KEY = 'ss:translate:activeRun';

function _saveToSessionStorage(): void {
	try {
		sessionStorage.setItem(SESSION_KEY, JSON.stringify({
			runId: _state.runId,
			jobId: _state.jobId,
			isFullRun: _state.isFullRun,
			timestamp: Date.now(),
		}));
	} catch {
		// sessionStorage may be unavailable
	}
}

function _clearSessionStorage(): void {
	try {
		sessionStorage.removeItem(SESSION_KEY);
	} catch {
		// sessionStorage may be unavailable
	}
}

export function clearOnCompleteCallback(): void {
	_onCompleteCallback = null;
}

export function startTranslationRun(runId: string, options: StartRunOptions = {}): void {
	if (!runId) return;

	stopTranslationRun();

	_state = {
		...initialState,
		runId,
		uxState: 'running',
		jobId: options.jobId || null,
		isFullRun: options.isFullRun || false,
	};
	_notify();

	_onCompleteCallback = options.onComplete || null;
	_reconnectCount = 0;

	_saveToSessionStorage();
	_connectWebSocket(runId);
	_startTimeout();
}

/**
 * Reconnect to a running translation run (e.g. after page reload).
 * Unlike startTranslationRun, this does NOT clear the store first —
 * it re-establishes the WS to receive live progress.
 */
export function reconnectToRun(runId: string, options: StartRunOptions = {}): void {
	if (!runId) return;

	// If already connected to this run, skip
	if (_ws && _state.runId === runId) return;

	stopTranslationRun();

	_state = {
		..._state,
		runId,
		uxState: 'running',
		jobId: options.jobId || _state.jobId || null,
		isFullRun: options.isFullRun || _state.isFullRun || false,
	};
	_notify();

	_onCompleteCallback = options.onComplete || _onCompleteCallback;
	_reconnectCount = 0;

	_saveToSessionStorage();
	_connectWebSocket(runId);
	_startTimeout();
}

function _startTimeout(): void {
	_clearTimeout();
	_timeoutTimer = setTimeout(() => {
		log("TranslationRunStore", "EXPLORE", "Run timed out after 10 min — marking as failed", { runId: _state.runId }, "Timeout");
		_cleanup();
		_state = { ..._state, uxState: 'failed' };
		_notify();
		if (_onCompleteCallback) _onCompleteCallback({ status: 'TIMEOUT', error_message: 'Translation run timed out' });
	}, RUN_TIMEOUT_MS);
}

function _clearTimeout(): void {
	if (_timeoutTimer) {
		clearTimeout(_timeoutTimer);
		_timeoutTimer = null;
	}
}

function _clearReconnect(): void {
	if (_reconnectTimer) {
		clearTimeout(_reconnectTimer);
		_reconnectTimer = null;
	}
}

function _cleanup(): void {
	_clearTimeout();
	_clearReconnect();
	_clearSessionStorage();
	if (_ws) {
		_ws.onclose = null;
		_ws.onerror = null;
		_ws.close();
		_ws = null;
	}
}

function _handleWsFailure(reason: string): void {
	log("TranslationRunStore", "EXPLORE", "WebSocket connection lost", { runId: _state.runId, reason, attempt: _reconnectCount + 1 }, String(reason));

	if (_state.uxState === 'failed' || _state.uxState === 'completed' || _state.uxState === 'cancelled') {
		return; // Already terminal — don't reconnect
	}

	_reconnectCount++;
	if (_reconnectCount <= MAX_RECONNECT) {
		log("TranslationRunStore", "REASON", "Reconnecting WebSocket", { runId: _state.runId, attempt: _reconnectCount });
		_reconnectTimer = setTimeout(() => {
			if (_state.runId) _connectWebSocket(_state.runId);
		}, RECONNECT_DELAY_MS);
	} else {
		log("TranslationRunStore", "EXPLORE", "Max reconnects exhausted — marking as failed", { runId: _state.runId }, "Max reconnects exhausted");
		_cleanup();
		_state = { ..._state, uxState: 'failed' };
		_notify();
		if (_onCompleteCallback) _onCompleteCallback({ status: 'WS_FAILED', error_message: 'WebSocket connection lost after max reconnects' });
	}
}

function _connectWebSocket(runId: string): void {
	try {
		const wsUrl = getTranslateRunWsUrl(runId);
		_ws = new WebSocket(wsUrl);

		_ws.onopen = () => {
			log("TranslationRunStore", "REASON", "WebSocket connected", { runId });
			_reconnectCount = 0; // Reset on successful connect
		};

		_ws.onmessage = (event: MessageEvent) => {
			try {
				const data = JSON.parse(event.data) as Record<string, unknown>;
				if (data.error) return;
				const s = data.status as string;
				const total = (data.total_records as number) || 0;
				const done = ((data.successful_records as number) || 0) + ((data.failed_records as number) || 0) + ((data.skipped_records as number) || 0);
				const pct = total > 0 ? Math.round((done / total) * 100) : 0;
				const insertS = data.insert_status as string;
				const newUxState: UxState | null =
					s === 'PENDING' || s === 'RUNNING'
						? (insertS === 'started' || insertS === 'pending' || insertS === 'running' ? 'inserting' : 'running')
						: s === 'COMPLETED'
							? (insertS === 'failed' || insertS === 'timeout' ? 'insert_failed' : ((data.failed_records as number) > 0 ? 'partial' : 'completed'))
							: s === 'FAILED' ? 'failed'
							: s === 'CANCELLED' ? 'cancelled'
							: null;
				if (newUxState) {
					_state = {
						..._state,
						uxState: newUxState,
						status: data,
						totalRecords: total,
						successfulRecords: (data.successful_records as number) || 0,
						failedRecords: (data.failed_records as number) || 0,
						skippedRecords: (data.skipped_records as number) || 0,
						cacheHits: (data.cache_hits as number) || 0,
						progressPct: pct,
						insertStatus: insertS || null,
						batchCount: (data.batch_count as number) || 0,
					};
					_notify();
					if (newUxState !== 'running' && newUxState !== 'inserting') {
						_cleanup(); // Terminal — stop reconnect timer + timeout
						if (_onCompleteCallback) _onCompleteCallback(data);
					}
				}
			} catch {
				// Ignore parse errors
			}
		};

		_ws.onerror = () => {
			_ws = null;
			// Don't call _handleWsFailure here — onclose will fire next
		};

		_ws.onclose = (event: CloseEvent) => {
			log("TranslationRunStore", "REFLECT", "WebSocket closed", { runId, code: event.code, reason: event.reason });
			_ws = null;
			if (event.code !== 1000 && event.code !== 1005) {
				_handleWsFailure(`close code ${event.code}`);
			}
		};
	} catch (_e) {
		log("TranslationRunStore", "EXPLORE", "Failed to create WebSocket", { runId }, String(_e));
		_ws = null;
		_handleWsFailure('constructor failed');
	}
}

export function stopTranslationRun(): void {
	_cleanup();
}

export function resetTranslationRun(): void {
	stopTranslationRun();
	_reconnectCount = 0;
	_clearSessionStorage();
	_state = initialState;
	_notify();
	_onCompleteCallback = null;
}

/** Read persisted active run info from sessionStorage (survives page reload). */
export function getStoredActiveRun(): { runId: string; jobId?: string; isFullRun?: boolean } | null {
	try {
		const raw = sessionStorage.getItem(SESSION_KEY);
		if (!raw) return null;
		const data = JSON.parse(raw);
		if (!data?.runId) return null;
		return data;
	} catch {
		return null;
	}
}

export function updateTranslationRunState(state: Partial<TranslationRunState>): void {
	_state = { ..._state, ...state };
	_notify();
}

// #endregion TranslationRunStore
