import type GraphContainer from '@gitkraken/gitkraken-components';
import type {
	ColumnNumberBySha,
	GraphRef,
	GraphRow,
	GraphSelectionState,
	GraphZoneType,
	ReadonlyGraphRow,
} from '@gitkraken/gitkraken-components';
import { html, LitElement } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { createElement } from 'react';
import { createRoot } from 'react-dom/client';
import { debounce } from '@gitlens/utils/debounce.js';
import type {
	GraphAvatars,
	GraphColumnsConfig,
	GraphExcludedRef,
	GraphMissingRefsMetadata,
	GraphRefMetadataItem,
	RowAction,
} from '../../../../plus/graph/protocol.js';
import type { GraphWrapperInitProps, GraphWrapperProps, GraphWrapperSubscriberProps } from './gl-graph.react.jsx';
import { GlGraphReact } from './gl-graph.react.jsx';

/**
 * A LitElement web component that encapsulates the GraphWrapperReact component.
 * This component mounts the React component once and then updates its state
 * without remounting on subsequent property changes.
 */
@customElement('gl-graph')
export class GlGraph extends LitElement {
	// Use Light DOM instead of Shadow DOM to avoid styling issues
	protected override createRenderRoot(): HTMLElement | DocumentFragment {
		return this;
	}

	// React root for mounting the React component
	private reactRoot: ReturnType<typeof createRoot> | null = null;

	// State updater function provided by the React component
	private provideReactState: ((props: Partial<GraphWrapperSubscriberProps>) => void) | null = null;
	private setReactStateProvider = (updater: (props: Partial<GraphWrapperSubscriberProps>) => void) => {
		this.provideReactState = updater;
	};

	// Properties that match GraphWrapperProps
	@property({ attribute: false })
	activeFilterColumns?: GraphWrapperProps['activeFilterColumns'];

	@property({ type: String })
	activeRow?: GraphWrapperProps['activeRow'];

	@property({ type: String })
	repoPath?: GraphWrapperProps['repoPath'];

	@property({ type: Object })
	avatars?: GraphWrapperProps['avatars'];

	@property({ type: Object })
	columns?: GraphWrapperProps['columns'];

	@property({ type: Object })
	context?: GraphWrapperProps['context'];

	@property({ type: Object })
	config?: GraphWrapperProps['config'];

	@property({ type: Object })
	downstreams?: GraphWrapperProps['downstreams'];

	@property({ type: Array })
	rows?: GraphWrapperProps['rows'];

	@property({ type: Object })
	excludeRefs?: GraphWrapperProps['excludeRefs'];

	@property({ type: Object })
	excludeTypes?: GraphWrapperProps['excludeTypes'];

	// @property({ type: String })
	// override nonce?: string;

	@property({ type: Object })
	paging?: GraphWrapperProps['paging'];

	@property({ type: Boolean })
	loading?: GraphWrapperProps['loading'];

	@property({ type: Object })
	selectedRows?: GraphWrapperProps['selectedRows'];

	@property({ type: Boolean })
	windowFocused?: GraphWrapperProps['windowFocused'];

	@property({ type: Object })
	refsMetadata?: GraphWrapperProps['refsMetadata'];

	@property({ type: Object })
	includeOnlyRefs?: GraphWrapperProps['includeOnlyRefs'];

	@property({ type: Object })
	pinnedRef?: GraphWrapperProps['pinnedRef'];

	@property({ type: Object })
	rowsStats?: GraphWrapperProps['rowsStats'];

	@property({ type: Boolean })
	rowsStatsLoading?: GraphWrapperProps['rowsStatsLoading'];

	@property({ type: Object })
	workingTreeStats?: GraphWrapperProps['workingTreeStats'];

	@property({ type: Object })
	runningOperationByRowSha?: GraphWrapperProps['runningOperationByRowSha'];

	@property({ type: Object })
	agentStatusByRowSha?: GraphWrapperProps['agentStatusByRowSha'];

	@property({ type: Object })
	unpublishedShas?: GraphWrapperProps['unpublishedShas'];

	@property({ type: Object })
	wipMetadataBySha?: GraphWrapperProps['wipMetadataBySha'];

	@property({ type: Number, attribute: 'wip-shas-settle-delay-ms' })
	wipShasSettleDelayMs?: number;

	@property({ type: String, attribute: 'wip-visibility' })
	wipVisibility?: GraphWrapperProps['wipVisibility'];

	@property({ type: Object })
	scope?: GraphWrapperProps['scope'];

	@property({ type: Object })
	theming?: GraphWrapperProps['theming'];

	@property({ type: String })
	searchMode?: GraphWrapperProps['searchMode'];

	@property({ type: Object })
	searchResults?: GraphWrapperProps['searchResults'];

	@property({ attribute: false })
	setRef!: (ref: GraphContainer) => void;

	// Clean up React component when disconnected
	override disconnectedCallback(): void {
		if (this.reactRoot) {
			this.reactRoot.unmount();
			this.reactRoot = null;
		}
		super.disconnectedCallback?.();
	}

	private changedProps: Map<string, unknown> = new Map();
	private updateScheduled: boolean = false;

	override shouldUpdate(changedProperties: Map<string, unknown>): boolean {
		if (!this.provideReactState) return this.reactRoot == null;

		for (const key of changedProperties.keys() as Iterable<keyof GraphWrapperSubscriberProps>) {
			this.changedProps.set(key, this[key]);
		}

		// Debounce updates to avoid rapid re-renders
		if (this.updateScheduled) return this.reactRoot == null;

		this.updateScheduled = true;

		const { provideReactState: stateUpdater } = this;
		requestAnimationFrame(() => {
			this.updateScheduled = false;
			if (this.changedProps.size > 0) {
				stateUpdater(Object.fromEntries(this.changedProps));
				this.changedProps.clear();
			}
		});
		return this.reactRoot == null;
	}

	override firstUpdated(): void {
		// Create a React root
		this.reactRoot = createRoot(this.querySelector('.graph__graph-root')!);

		// Mount the React component
		this.reactRoot.render(
			createElement(GlGraphReact, {
				setRef: this.setRef,
				subscriber: this.setReactStateProvider,

				activeFilterColumns: this.activeFilterColumns ?? new Set(),
				activeRow: this.activeRow,
				avatars: this.avatars,
				columns: this.columns,
				config: this.config,
				context: this.context,
				downstreams: this.downstreams,
				excludeRefs: this.excludeRefs,
				excludeTypes: this.excludeTypes,
				includeOnlyRefs: this.includeOnlyRefs,
				pinnedRef: this.pinnedRef,
				loading: this.loading,
				nonce: this.nonce,
				paging: this.paging,
				refsMetadata: this.refsMetadata,
				rows: this.rows,
				rowsStats: this.rowsStats,
				rowsStatsLoading: this.rowsStatsLoading,
				searchMode: this.searchMode ?? 'normal',
				searchResults: this.searchResults,
				selectedRows: this.selectedRows,
				repoPath: this.repoPath,
				theming: this.theming,
				windowFocused: this.windowFocused,
				workingTreeStats: this.workingTreeStats,
				wipMetadataBySha: this.wipMetadataBySha,
				wipShasSettleDelayMs: this.wipShasSettleDelayMs,
				wipVisibility: this.wipVisibility,
				scope: this.scope,
				runningOperationByRowSha: this.runningOperationByRowSha,
				agentStatusByRowSha: this.agentStatusByRowSha,
				unpublishedShas: this.unpublishedShas,

				onChangeColumns: this.handleChangeColumns,
				onScopeAnchorsUnreachable: this.handleScopeAnchorsUnreachable,
				onWipShasMissingStats: this.handleWipShasMissingStats,
				onVisibleWipShasChanged: this.handleVisibleWipShasChanged,
				onColumnsCalculated: this.handleColumnsCalculated,
				onChangeRefsVisibility: this.handleChangeRefsVisibility,
				onChangeSelection: this.handleChangeSelection,
				onChangeVisibleDays: this.handleChangeVisibleDays,
				onFilterColumn: this.handleFilterColumn,
				onAvatarLoadError: this.handleAvatarLoadError,
				onMissingAvatars: this.handleMissingAvatars,
				onMissingRefsMetadata: this.handleMissingRefsMetadata,
				onMoreRows: this.handleMoreRows,
				onMouseLeave: this.handleMouseLeave,
				onRefDoubleClick: this.handleRefDoubleClick,
				onRowAction: this.handleRowAction,
				onWipRowOpen: this.handleWipRowOpen,
				onRowContextMenu: this.handleRowContextMenu,
				onRowDoubleClick: this.handleRowDoubleClick,
				onRowHover: this.handleRowHover,
				onRowUnhover: this.handleRowUnhover,
				onRowActionHover: this.handleRowActionHover,
			} satisfies GraphWrapperInitProps),
		);
	}

	override render() {
		return html`<div class="graph__graph-root"></div>`;
	}

	// Event handlers that dispatch custom events
	private handleChangeColumns = debounce((columns: GraphColumnsConfig): void => {
		this.dispatchEvent(new CustomEvent('changecolumns', { detail: { settings: columns } }));
	}, 250);

	private handleChangeRefsVisibility = (detail: { refs: GraphExcludedRef[]; visible: boolean }): void => {
		this.dispatchEvent(new CustomEvent('changerefsvisibility', { detail: detail }));
	};

	private handleChangeSelection = debounce(
		(rows: ReadonlyGraphRow[], focusedRow: ReadonlyGraphRow | undefined, state: GraphSelectionState): void =>
			void this.dispatchEvent(
				new CustomEvent('changeselection', { detail: { rows: rows, focusedRow: focusedRow, state: state } }),
			),
		50,
		{ edges: 'both' },
	);

	private handleChangeVisibleDays = (detail: { top: number; bottom: number }): void => {
		this.dispatchEvent(new CustomEvent('changevisibledays', { detail: detail }));
	};

	private handleFilterColumn = (detail: { zone: GraphZoneType }): void => {
		this.dispatchEvent(new CustomEvent('filtercolumn', { detail: detail }));
	};

	private handleAvatarLoadError = (emails: GraphAvatars): void => {
		this.dispatchEvent(new CustomEvent('avatarloaderror', { detail: emails }));
	};

	private handleMissingAvatars = (emails: GraphAvatars): void => {
		this.dispatchEvent(new CustomEvent('missingavatars', { detail: emails }));
	};

	private handleMissingRefsMetadata = (metadata: GraphMissingRefsMetadata): void => {
		this.dispatchEvent(new CustomEvent('missingrefsmetadata', { detail: metadata }));
	};

	private handleMoreRows = (id?: string): void => {
		this.dispatchEvent(new CustomEvent('morerows', { detail: id }));
	};

	private handleMouseLeave = (): void => {
		this.dispatchEvent(new CustomEvent('graphmouseleave'));
	};

	private handleRefDoubleClick = (detail: { ref: GraphRef; metadata?: GraphRefMetadataItem }): void => {
		this.dispatchEvent(new CustomEvent('refdoubleclick', { detail: detail }));
	};

	private handleRowAction = (detail: { action: RowAction; row: GraphRow; worktreePath?: string }): void => {
		this.dispatchEvent(new CustomEvent('rowaction', { detail: detail }));
	};

	private handleWipRowOpen = (detail: {
		target: 'compose' | 'review' | 'resolve' | 'agents';
		row: GraphRow;
	}): void => {
		this.dispatchEvent(new CustomEvent('wiprowopen', { detail: detail }));
	};

	private handleRowContextMenu = (detail: {
		graphZoneType: GraphZoneType;
		graphRow: GraphRow;
		isAvatar: boolean;
	}): void => {
		this.dispatchEvent(new CustomEvent('rowcontextmenu', { detail: detail }));
	};

	private handleRowDoubleClick = (detail: { row: GraphRow; preserveFocus?: boolean }): void => {
		this.dispatchEvent(new CustomEvent('rowdoubleclick', { detail: detail }));
	};

	private _handleRowHoverDebounced = debounce(
		(detail: {
			graphZoneType: GraphZoneType;
			graphRow: GraphRow;
			clientX: number;
			currentTarget: HTMLElement;
		}): void => {
			this.dispatchEvent(new CustomEvent('rowhover', { detail: detail }));
		},
		250,
	);

	private handleRowHover = (detail: {
		graphZoneType: GraphZoneType;
		graphRow: GraphRow;
		clientX: number;
		currentTarget: HTMLElement;
	}): void => {
		// Fire immediately so downstream can cancel unhover timers before the debounced hover arrives
		this.dispatchEvent(new CustomEvent('rowhoverstart', { bubbles: true, composed: true }));
		// Lightweight immediate fire for consumers (e.g. the minimap) that need to track the hovered
		// row without waiting for the 250ms debounce below — the debounced `rowhover` is cancelled by
		// fast row-to-row transit so any consumer relying on it would lose updates.
		this.dispatchEvent(
			new CustomEvent('rowhovertrack', {
				detail: { graphZoneType: detail.graphZoneType, graphRow: detail.graphRow },
				bubbles: true,
				composed: true,
			}),
		);
		this._handleRowHoverDebounced(detail);
	};

	private handleRowUnhover = (detail: {
		graphZoneType: GraphZoneType;
		graphRow: GraphRow;
		relatedTarget: EventTarget | null;
	}): void => {
		this._handleRowHoverDebounced.cancel();
		this.dispatchEvent(new CustomEvent('rowunhover', { detail: detail }));
	};

	private handleRowActionHover = () => {
		this._handleRowHoverDebounced.cancel();
		this.dispatchEvent(new CustomEvent('row-action-hover', { bubbles: true, composed: true }));
	};

	private handleScopeAnchorsUnreachable = (unreachableAnchors: Set<string>): void => {
		this.dispatchEvent(new CustomEvent('scopeanchorsunreachable', { detail: unreachableAnchors }));
	};

	private handleWipShasMissingStats = (shas: Record<string, true>): void => {
		this.dispatchEvent(new CustomEvent('wipshasmissingstats', { detail: shas }));
	};

	private handleVisibleWipShasChanged = (shas: Record<string, true>): void => {
		this.dispatchEvent(new CustomEvent('visiblewipshaschanged', { detail: shas }));
	};

	private handleColumnsCalculated = (columnsBySha: ColumnNumberBySha): void => {
		this.dispatchEvent(new CustomEvent('columnscalculated', { detail: columnsBySha }));
	};
}

// Define the element in the custom elements registry
declare global {
	interface HTMLElementTagNameMap {
		'gl-graph': GlGraph;
	}

	interface GlobalEventHandlersEventMap {
		changecolumns: CustomEvent<{ settings: GraphColumnsConfig }>;
		changerefsvisibility: CustomEvent<{ refs: GraphExcludedRef[]; visible: boolean }>;
		changeselection: CustomEvent<{
			rows: ReadonlyGraphRow[];
			focusedRow: ReadonlyGraphRow | undefined;
			state: GraphSelectionState;
		}>;
		changevisibledays: CustomEvent<{ top: number; bottom: number }>;
		avatarloaderror: CustomEvent<GraphAvatars>;
		missingavatars: CustomEvent<GraphAvatars>;
		missingrefsmetadata: CustomEvent<GraphMissingRefsMetadata>;
		morerows: CustomEvent<string | undefined>;
		refdoubleclick: CustomEvent<{ ref: GraphRef; metadata?: GraphRefMetadataItem }>;
		rowaction: CustomEvent<{ action: RowAction; row: GraphRow; worktreePath?: string }>;
		rowcontextmenu: CustomEvent<{ graphZoneType: GraphZoneType; graphRow: GraphRow; isAvatar: boolean }>;
		rowdoubleclick: CustomEvent<{ row: GraphRow; preserveFocus?: boolean }>;
		rowhover: CustomEvent<{
			graphZoneType: GraphZoneType;
			graphRow: GraphRow;
			clientX: number;
			currentTarget: HTMLElement;
		}>;
		rowhoverstart: CustomEvent<void>;
		rowhovertrack: CustomEvent<{
			graphZoneType: GraphZoneType;
			graphRow: GraphRow;
		}>;
		rowunhover: CustomEvent<{
			graphZoneType: GraphZoneType;
			graphRow: GraphRow;
			relatedTarget: EventTarget | null;
		}>;
		scopeanchorsunreachable: CustomEvent<Set<string>>;
		wipshasmissingstats: CustomEvent<Record<string, true>>;
		visiblewipshaschanged: CustomEvent<Record<string, true>>;
		columnscalculated: CustomEvent<ColumnNumberBySha>;
	}
}
