/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import './media/agentsessionsviewer.css';
import { h } from '../../../../../base/browser/dom.js';
import { localize } from '../../../../../nls.js';
import { IIdentityProvider, IListVirtualDelegate, NotSelectableGroupId, NotSelectableGroupIdType } from '../../../../../base/browser/ui/list/list.js';
import { AriaRole } from '../../../../../base/browser/ui/aria/aria.js';
import { IListAccessibilityProvider } from '../../../../../base/browser/ui/list/listWidget.js';
import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js';
import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js';
import { ICompressibleKeyboardNavigationLabelProvider, ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js';
import { ITreeNode, ITreeElementRenderDetails, IAsyncDataSource, ITreeSorter, ITreeDragAndDrop, ITreeDragOverReaction } from '../../../../../base/browser/ui/tree/tree.js';
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { AgentSessionSection, AgentSessionStatus, getAgentChangesSummary, hasValidDiff, IAgentSession, IAgentSessionSection, IAgentSessionsModel, isAgentSession, isAgentSessionSection, isAgentSessionsModel, isSessionInProgressStatus } from './agentSessionsModel.js';
import { IconLabel } from '../../../../../base/browser/ui/iconLabel/iconLabel.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { fromNow, getDurationString } from '../../../../../base/common/date.js';
import { FuzzyScore, createMatches } from '../../../../../base/common/filters.js';
import { IMarkdownRendererService } from '../../../../../platform/markdown/browser/markdownRenderer.js';
import { allowedChatMarkdownHtmlTags } from '../widget/chatContentMarkdownRenderer.js';
import { IProductService } from '../../../../../platform/product/common/productService.js';
import { IDragAndDropData } from '../../../../../base/browser/dnd.js';
import { ListViewTargetSector } from '../../../../../base/browser/ui/list/listView.js';
import { coalesce } from '../../../../../base/common/arrays.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { fillEditorsDragData } from '../../../../browser/dnd.js';
import { HoverStyle, IDelayedHoverOptions } from '../../../../../base/browser/ui/hover/hover.js';
import { HoverPosition } from '../../../../../base/browser/ui/hover/hoverWidget.js';
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
import { IntervalTimer } from '../../../../../base/common/async.js';
import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
import { MenuId } from '../../../../../platform/actions/common/actions.js';
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
import { Emitter, Event } from '../../../../../base/common/event.js';
import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';
import { MarkdownString, IMarkdownString } from '../../../../../base/common/htmlContent.js';
import { AgentSessionHoverWidget } from './agentSessionHoverWidget.js';
import { AgentSessionProviders, getAgentSessionTime } from './agentSessions.js';
import { AgentSessionsGrouping } from './agentSessionsFilter.js';
import { autorun } from '../../../../../base/common/observable.js';
import { Button } from '../../../../../base/browser/ui/button/button.js';
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
import { AgentSessionApprovalModel } from './agentSessionApprovalModel.js';
import { BugIndicatingError } from '../../../../../base/common/errors.js';


export type AgentSessionListItem = IAgentSession | IAgentSessionSection;

//#region Agent Session Renderer

interface IAgentSessionItemTemplate {
	readonly element: HTMLElement;

	// Column 1
	readonly icon: HTMLElement;

	// Column 2 Row 1
	readonly title: IconLabel;
	readonly statusContainer: HTMLElement;
	readonly statusProviderIcon: HTMLElement;
	readonly statusTime: HTMLElement;
	readonly titleToolbar: MenuWorkbenchToolBar;

	// Column 2 Row 2
	readonly diffContainer: HTMLElement;
	readonly diffAddedSpan: HTMLSpanElement;
	readonly diffRemovedSpan: HTMLSpanElement;

	readonly badge: HTMLElement;
	readonly separator: HTMLElement;
	readonly description: HTMLElement;

	// Approval row
	readonly approvalRow: HTMLElement;
	readonly approvalLabel: HTMLElement;
	readonly approvalButtonContainer: HTMLElement;

	readonly contextKeyService: IContextKeyService;
	readonly elementDisposable: DisposableStore;
	readonly disposables: IDisposable;
}

export interface IAgentSessionRendererOptions {
	readonly useSimpleHover?: boolean;
	readonly showIsolationIcon?: boolean;
	getHoverPosition(): HoverPosition;
}

export class AgentSessionRenderer extends Disposable implements ICompressibleTreeRenderer<IAgentSession, FuzzyScore, IAgentSessionItemTemplate> {

	static readonly TEMPLATE_ID = 'agent-session';

	static readonly APPROVAL_ROW_HEIGHT = 40;

	readonly templateId = AgentSessionRenderer.TEMPLATE_ID;

	private readonly sessionHover = this._register(new MutableDisposable<AgentSessionHoverWidget>());

	private readonly _onDidChangeItemHeight = this._register(new Emitter<IAgentSession>());
	readonly onDidChangeItemHeight: Event<IAgentSession> = this._onDidChangeItemHeight.event;

	constructor(
		private readonly options: IAgentSessionRendererOptions,
		private readonly _approvalModel: AgentSessionApprovalModel | undefined,
		@IMarkdownRendererService private readonly markdownRendererService: IMarkdownRendererService,
		@IProductService private readonly productService: IProductService,
		@IHoverService private readonly hoverService: IHoverService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
	) {
		super();
	}

	renderTemplate(container: HTMLElement): IAgentSessionItemTemplate {
		const disposables = new DisposableStore();
		const elementDisposable = disposables.add(new DisposableStore());

		const elements = h(
			'div.agent-session-item@item',
			[
				h('div.agent-session-icon-col', [
					h('div.agent-session-icon@icon')
				]),
				h('div.agent-session-main-col', [
					h('div.agent-session-title-row', [
						h('div.agent-session-title@title'),
						h('div.agent-session-title-toolbar@titleToolbar'),
					]),
					h('div.agent-session-details-row', [
						h('div.agent-session-diff-container@diffContainer',
							[
								h('span.agent-session-diff-added@addedSpan'),
								h('span.agent-session-diff-removed@removedSpan')
							]),
						h('div.agent-session-description@description'),
						h('div.agent-session-details-right', [
							h('div.agent-session-badge@badge'),
							h('span.agent-session-separator@separator'),
							h('div.agent-session-status@statusContainer', [
								h('span.agent-session-status-provider-icon@statusProviderIcon'),
								h('span.agent-session-status-time@statusTime')
							]),
						]),
					]),
					h('div.agent-session-approval-row@approvalRow', [
						h('span.agent-session-approval-label@approvalLabel'),
						h('div.agent-session-approval-button@approvalButtonContainer'),
					])
				])
			]
		);

		const contextKeyService = disposables.add(this.contextKeyService.createScoped(elements.item));
		const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));
		const titleToolbar = disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, elements.titleToolbar, MenuId.AgentSessionItemToolbar, {
			menuOptions: { shouldForwardArgs: true },
		}));

		container.appendChild(elements.item);

		return {
			element: elements.item,
			icon: elements.icon,
			title: disposables.add(new IconLabel(elements.title, { supportHighlights: true, supportIcons: true })),
			titleToolbar,
			diffContainer: elements.diffContainer,
			diffAddedSpan: elements.addedSpan,
			diffRemovedSpan: elements.removedSpan,
			badge: elements.badge,
			separator: elements.separator,
			description: elements.description,
			statusContainer: elements.statusContainer,
			statusProviderIcon: elements.statusProviderIcon,
			statusTime: elements.statusTime,
			approvalRow: elements.approvalRow,
			approvalLabel: elements.approvalLabel,
			approvalButtonContainer: elements.approvalButtonContainer,
			contextKeyService,
			elementDisposable,
			disposables
		};
	}

	renderElement(session: ITreeNode<IAgentSession, FuzzyScore>, index: number, template: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {

		// Clear old state
		template.elementDisposable.clear();
		template.diffAddedSpan.textContent = '';
		template.diffRemovedSpan.textContent = '';
		template.badge.textContent = '';
		template.description.textContent = '';

		// Archived
		template.element.classList.toggle('archived', session.element.isArchived());

		// Icon
		template.icon.className = `agent-session-icon ${ThemeIcon.asClassName(this.getIcon(session.element))}`;

		// Title
		const markdownTitle = new MarkdownString(session.element.label);
		template.title.setLabel(renderAsPlaintext(markdownTitle), undefined, { matches: createMatches(session.filterData) });

		// Title Actions - Update context keys
		ChatContextKeys.isArchivedAgentSession.bindTo(template.contextKeyService).set(session.element.isArchived());
		ChatContextKeys.isReadAgentSession.bindTo(template.contextKeyService).set(session.element.isRead());
		ChatContextKeys.agentSessionType.bindTo(template.contextKeyService).set(session.element.providerType);
		template.titleToolbar.context = session.element;

		// Diff information
		let hasDiff = false;
		const { changes: diff } = session.element;
		if (!isSessionInProgressStatus(session.element.status) && diff && hasValidDiff(diff)) {
			if (this.renderDiff(session, template)) {
				hasDiff = true;
			}
		}
		template.diffContainer.classList.toggle('has-diff', hasDiff);

		let hasAgentSessionChanges = false;
		if (
			session.element.providerType === AgentSessionProviders.Background ||
			session.element.providerType === AgentSessionProviders.Cloud
		) {
			// Background and Cloud agents provide the list of changes directly,
			// so we have to use the list of changes to determine whether to show
			// the "View All Changes" action
			hasAgentSessionChanges = Array.isArray(diff) && diff.length > 0;
		} else {
			hasAgentSessionChanges = hasDiff;
		}

		ChatContextKeys.hasAgentSessionChanges.bindTo(template.contextKeyService).set(hasAgentSessionChanges);

		// Badge
		const hasBadge = this.renderBadge(session, template);
		template.badge.classList.toggle('has-badge', hasBadge);

		// Description (unless diff is shown)
		if (!hasDiff) {
			this.renderDescription(session, template);
		}

		// Separator (dot between badge and timestamp)
		template.separator.classList.toggle('has-separator', hasBadge);

		// Status
		this.renderStatus(session, template);

		// Hover
		this.renderHover(session, template);

		// Approval row
		if (this._approvalModel) {
			this.renderApprovalRow(session, template);
		}
	}

	private renderBadge(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {
		const badge = session.element.badge;
		if (badge) {
			this.renderMarkdownOrText(badge, template.badge, template.elementDisposable);
		}

		return !!badge;
	}

	private renderMarkdownOrText(content: string | IMarkdownString, container: HTMLElement, disposables: DisposableStore): void {
		if (typeof content === 'string') {
			container.textContent = content;
		} else {
			disposables.add(this.markdownRendererService.render(content, {
				sanitizerConfig: {
					replaceWithPlaintext: true,
					allowedTags: {
						override: allowedChatMarkdownHtmlTags,
					},
					allowedLinkSchemes: { augment: [this.productService.urlProtocol] }
				},
			}, container));
		}
	}

	private renderDiff(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): boolean {
		const diff = getAgentChangesSummary(session.element.changes);
		if (!diff) {
			return false;
		}

		if (diff.insertions >= 0 /* render even `0` for more homogeneity */) {
			template.diffAddedSpan.textContent = `+${diff.insertions}`;
		}

		if (diff.deletions >= 0 /* render even `0` for more homogeneity */) {
			template.diffRemovedSpan.textContent = `-${diff.deletions}`;
		}

		return true;
	}

	private getIcon(session: IAgentSession): ThemeIcon {
		if (session.status === AgentSessionStatus.InProgress) {
			return Codicon.sessionInProgress;
		}

		if (session.status === AgentSessionStatus.NeedsInput) {
			return Codicon.report;
		}

		if (session.status === AgentSessionStatus.Failed) {
			return Codicon.error;
		}

		if (!session.isRead() && !session.isArchived()) {
			return Codicon.circleFilled;
		}

		return Codicon.circleSmallFilled;
	}

	private renderDescription(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
		const description = session.element.description;
		if (description) {
			this.renderMarkdownOrText(description, template.description, template.elementDisposable);
			return;
		}

		// Fallback to state label
		if (session.element.status === AgentSessionStatus.InProgress) {
			template.description.textContent = localize('chat.session.status.inProgress', "Working...");
		} else if (session.element.status === AgentSessionStatus.NeedsInput) {
			template.description.textContent = localize('chat.session.status.needsInput', "Input needed.");
		} else if (
			session.element.timing.lastRequestEnded &&
			session.element.timing.lastRequestStarted &&
			session.element.timing.lastRequestEnded > session.element.timing.lastRequestStarted
		) {
			const duration = this.toDuration(session.element.timing.lastRequestStarted, session.element.timing.lastRequestEnded, false, true);

			template.description.textContent = session.element.status === AgentSessionStatus.Failed ?
				localize('chat.session.status.failedAfter', "Failed after {0}", duration) :
				localize('chat.session.status.completedAfter', "Completed in {0}", duration);
		} else {
			template.description.textContent = session.element.status === AgentSessionStatus.Failed ?
				localize('chat.session.status.failed', "Failed") :
				localize('chat.session.status.completed', "Completed");
		}
	}

	private toDuration(startTime: number, endTime: number, useFullTimeWords: boolean, disallowNow: boolean): string {
		const elapsed = Math.max(Math.round((endTime - startTime) / 1000) * 1000, 1000 /* clamp to 1s */);
		if (!disallowNow && elapsed < 60000) {
			return localize('secondsDuration', "now");
		}

		return getDurationString(elapsed, useFullTimeWords);
	}

	private renderStatus(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {

		const getTimeLabel = (session: IAgentSession) => {
			let timeLabel: string | undefined;
			if (session.status === AgentSessionStatus.InProgress && session.timing.lastRequestStarted) {
				timeLabel = this.toDuration(session.timing.lastRequestStarted, Date.now(), false, false);
			}

			if (!timeLabel) {
				const date = getAgentSessionTime(session.timing);
				const seconds = Math.round((new Date().getTime() - date) / 1000);
				if (seconds < 60) {
					timeLabel = localize('secondsDuration', "now");
				} else {
					timeLabel = sessionDateFromNow(date);
				}
			}

			return timeLabel;
		};

		// Provider icon (only shown for non-local sessions)
		// When showIsolationIcon is enabled for background sessions, show worktree/folder icon instead
		const isLocal = session.element.providerType === AgentSessionProviders.Local;
		if (isLocal) {
			template.statusProviderIcon.className = '';
		} else if (this.options.showIsolationIcon && session.element.providerType === AgentSessionProviders.Background) {
			const hasWorktree = typeof session.element.metadata?.worktreePath === 'string';
			const isolationIcon = hasWorktree ? Codicon.worktree : Codicon.folder;
			template.statusProviderIcon.className = `agent-session-status-provider-icon ${ThemeIcon.asClassName(isolationIcon)}`;
		} else {
			template.statusProviderIcon.className = `agent-session-status-provider-icon ${ThemeIcon.asClassName(session.element.icon)}`;
		}

		// Time label
		template.statusTime.textContent = getTimeLabel(session.element);
		const timer = template.elementDisposable.add(new IntervalTimer());
		timer.cancelAndSet(() => template.statusTime.textContent = getTimeLabel(session.element), session.element.status === AgentSessionStatus.InProgress ? 1000 /* every second */ : 60 * 1000 /* every minute */);
	}

	private renderHover(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
		if (this.options.useSimpleHover) {
			const title = renderAsPlaintext(new MarkdownString(session.element.label));
			template.elementDisposable.add(this.hoverService.setupDelayedHover(template.element, { content: title, position: { hoverPosition: this.options.getHoverPosition() } }, { groupId: 'agent.sessions' }));
			return;
		}

		if (!isSessionInProgressStatus(session.element.status) && session.element.isRead()) {
			return; // the hover is complex and large, for now limit it to in-progress sessions only
		}

		const reducedDelay = session.element.status === AgentSessionStatus.NeedsInput;
		template.elementDisposable.add(
			this.hoverService.setupDelayedHover(template.element, () => this.buildHoverContent(session.element), { groupId: 'agent.sessions', reducedDelay })
		);
	}

	private buildHoverContent(session: IAgentSession): IDelayedHoverOptions {
		if (this.sessionHover.value?.session.resource.toString() !== session.resource.toString()) {
			// note: hover service use mouseover which triggers again if the mouse moves
			// within the element. Only recreate the hover widget if the session changed.
			this.sessionHover.value = this.instantiationService.createInstance(AgentSessionHoverWidget, session);
		}

		const widget = this.sessionHover.value;
		return {
			id: `agent.session.hover.${session.resource.toString()}`,
			content: widget.domNode,
			style: HoverStyle.Pointer,
			onDidShow: () => widget.onRendered(),
			position: {
				hoverPosition: this.options.getHoverPosition()
			}
		};
	}

	private renderApprovalRow(session: ITreeNode<IAgentSession, FuzzyScore>, template: IAgentSessionItemTemplate): void {
		if (this._approvalModel === undefined) {
			throw new BugIndicatingError('Approval model is required to render approval row');
		}

		const approvalModel = this._approvalModel;
		// Initialize from current model state to avoid unnecessary height changes on first render
		const initialInfo = approvalModel.getApproval(session.element.resource).get();
		let wasVisible = !!initialInfo;
		template.approvalRow.classList.toggle('visible', wasVisible);

		const buttonStore = template.elementDisposable.add(new DisposableStore());

		template.elementDisposable.add(autorun(reader => {
			buttonStore.clear();

			const info = approvalModel.getApproval(session.element.resource).read(reader);
			const visible = !!info;

			template.approvalRow.classList.toggle('visible', visible);

			if (info) {
				// Render as a syntax-highlighted code block
				const codeblockContent = new MarkdownString().appendCodeblock(info.languageId ?? 'json', info.label);
				this.renderMarkdownOrText(codeblockContent, template.approvalLabel, buttonStore);

				// Hover with full content as a code block
				buttonStore.add(this.hoverService.setupDelayedHover(template.approvalLabel, {
					content: codeblockContent,
					style: HoverStyle.Pointer,
					position: { hoverPosition: HoverPosition.BELOW },
				}));

				template.approvalButtonContainer.textContent = '';
				const button = buttonStore.add(new Button(template.approvalButtonContainer, {
					title: localize('allowActionOnce', "Allow once"),
					...defaultButtonStyles
				}));
				button.label = localize('allowAction', "Allow");
				buttonStore.add(button.onDidClick(() => info.confirm()));
			}

			if (wasVisible !== visible) {
				wasVisible = visible;
				this._onDidChangeItemHeight.fire(session.element);
			}
		}));
	}

	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IAgentSession>, FuzzyScore>, index: number, templateData: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {
		throw new Error('Should never happen since session is incompressible');
	}

	disposeElement(element: ITreeNode<IAgentSession, FuzzyScore>, index: number, template: IAgentSessionItemTemplate, details?: ITreeElementRenderDetails): void {
		template.elementDisposable.clear();
	}

	disposeTemplate(templateData: IAgentSessionItemTemplate): void {
		templateData.disposables.dispose();
	}
}

export function toStatusLabel(status: AgentSessionStatus): string {
	let statusLabel: string;
	switch (status) {
		case AgentSessionStatus.NeedsInput:
			statusLabel = localize('agentSessionNeedsInput', "Needs Input");
			break;
		case AgentSessionStatus.InProgress:
			statusLabel = localize('agentSessionInProgress', "In Progress");
			break;
		case AgentSessionStatus.Failed:
			statusLabel = localize('agentSessionFailed', "Failed");
			break;
		default:
			statusLabel = localize('agentSessionCompleted', "Completed");
	}

	return statusLabel;
}

//#endregion

//#region Section Header Renderer

interface IAgentSessionSectionTemplate {
	readonly container: HTMLElement;
	readonly label: HTMLSpanElement;
	readonly toolbar: MenuWorkbenchToolBar;
	readonly contextKeyService: IContextKeyService;
	readonly disposables: IDisposable;
}

export class AgentSessionSectionRenderer implements ICompressibleTreeRenderer<IAgentSessionSection, FuzzyScore, IAgentSessionSectionTemplate> {

	static readonly TEMPLATE_ID = 'agent-session-section';

	readonly templateId = AgentSessionSectionRenderer.TEMPLATE_ID;

	constructor(
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IContextKeyService private readonly contextKeyService: IContextKeyService,
	) { }

	renderTemplate(container: HTMLElement): IAgentSessionSectionTemplate {
		const disposables = new DisposableStore();

		const elements = h(
			'div.agent-session-section@container',
			[
				h('span.agent-session-section-label@label'),
				h('div.agent-session-section-toolbar@toolbar')
			]
		);

		const contextKeyService = disposables.add(this.contextKeyService.createScoped(elements.container));
		const scopedInstantiationService = disposables.add(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])));
		const toolbar = disposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, elements.toolbar, MenuId.AgentSessionSectionToolbar, {
			menuOptions: { shouldForwardArgs: true },
		}));

		container.appendChild(elements.container);

		return {
			container: elements.container,
			label: elements.label,
			toolbar,
			contextKeyService,
			disposables
		};
	}

	renderElement(element: ITreeNode<IAgentSessionSection, FuzzyScore>, index: number, template: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {

		// Label
		template.label.textContent = element.element.label;

		// Toolbar
		ChatContextKeys.agentSessionSection.bindTo(template.contextKeyService).set(element.element.section);
		template.toolbar.context = element.element;
	}

	renderCompressedElements(node: ITreeNode<ICompressedTreeNode<IAgentSessionSection>, FuzzyScore>, index: number, templateData: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {
		throw new Error('Should never happen since section header is incompressible');
	}

	disposeElement(element: ITreeNode<IAgentSessionSection, FuzzyScore>, index: number, template: IAgentSessionSectionTemplate, details?: ITreeElementRenderDetails): void {
		// noop
	}

	disposeTemplate(templateData: IAgentSessionSectionTemplate): void {
		templateData.disposables.dispose();
	}
}

//#endregion

export class AgentSessionsListDelegate implements IListVirtualDelegate<AgentSessionListItem> {

	static readonly ITEM_HEIGHT = 54;
	static readonly SECTION_HEIGHT = 26;

	constructor(private readonly _approvalModel?: AgentSessionApprovalModel) { }

	getHeight(element: AgentSessionListItem): number {
		if (isAgentSessionSection(element)) {
			return AgentSessionsListDelegate.SECTION_HEIGHT;
		}

		let height = AgentSessionsListDelegate.ITEM_HEIGHT;
		if (this._approvalModel?.getApproval(element.resource).get()) {
			height += AgentSessionRenderer.APPROVAL_ROW_HEIGHT;
		}
		return height;
	}

	hasDynamicHeight(element: AgentSessionListItem): boolean {
		return !!this._approvalModel && isAgentSession(element);
	}

	getTemplateId(element: AgentSessionListItem): string {
		if (isAgentSessionSection(element)) {
			return AgentSessionSectionRenderer.TEMPLATE_ID;
		}

		return AgentSessionRenderer.TEMPLATE_ID;
	}
}

export class AgentSessionsAccessibilityProvider implements IListAccessibilityProvider<AgentSessionListItem> {

	getWidgetRole(): AriaRole {
		return 'list';
	}

	getRole(element: AgentSessionListItem): AriaRole | undefined {
		return 'listitem';
	}

	getWidgetAriaLabel(): string {
		return localize('agentSessions', "Agent Sessions");
	}

	getAriaLabel(element: AgentSessionListItem): string | null {
		if (isAgentSessionSection(element)) {
			return localize('agentSessionSectionAriaLabel', "{0} sessions section", element.label);
		}

		return localize('agentSessionItemAriaLabel', "{0} session {1} ({2}), created {3}", element.providerLabel, element.label, toStatusLabel(element.status), new Date(element.timing.created).toLocaleString());
	}
}

export interface IAgentSessionsFilterExcludes {
	readonly providers: readonly string[];
	readonly states: readonly AgentSessionStatus[];

	readonly archived: boolean;
	readonly read: boolean;
}

export interface IAgentSessionsFilter {

	/**
	 * An event that fires when the filter changes and sessions
	 * should be re-evaluated.
	 */
	readonly onDidChange: Event<void>;

	/**
	 * Optional limit on the number of sessions to show.
	 */
	readonly limitResults?: () => number | undefined;

	/**
	 * Whether to show section headers to group sessions.
	 * When undefined, sessions are shown as a flat list.
	 */
	readonly groupResults?: () => AgentSessionsGrouping | undefined;

	/**
	 * A callback to notify the filter about the number of
	 * results after filtering.
	 */
	notifyResults?(count: number): void;

	/**
	 * The logic to exclude sessions from the view.
	 */
	exclude(session: IAgentSession): boolean;

	/**
	 * Get the current filter excludes for display in the UI.
	 */
	getExcludes(): IAgentSessionsFilterExcludes;

	/**
	 * Whether the filter is at its default state (no custom filters applied).
	 */
	isDefault(): boolean;

	/**
	 * Reset the filter to its default state.
	 */
	reset(): void;
}

export class AgentSessionsDataSource extends Disposable implements IAsyncDataSource<IAgentSessionsModel, AgentSessionListItem> {

	private static readonly CAPPED_SESSIONS_LIMIT = 3;

	private readonly _onDidGetChildren = this._register(new Emitter<number>());
	readonly onDidGetChildren: Event<number> = this._onDidGetChildren.event;

	constructor(
		private readonly filter: IAgentSessionsFilter | undefined,
		private readonly sorter: ITreeSorter<IAgentSession>,
	) {
		super();
	}

	hasChildren(element: IAgentSessionsModel | AgentSessionListItem): boolean {

		// Sessions model
		if (isAgentSessionsModel(element)) {
			return true;
		}

		// Sessions	section
		else if (isAgentSessionSection(element)) {
			return element.sessions.length > 0;
		}

		// Session element
		else {
			return false;
		}
	}

	getChildren(element: IAgentSessionsModel | AgentSessionListItem): Iterable<AgentSessionListItem> {

		// Sessions model
		if (isAgentSessionsModel(element)) {

			// Apply filter if configured
			let filteredSessions = element.sessions.filter(session => !this.filter?.exclude(session));

			// Apply sorter unless we group into sections or we are to limit results
			const limitResultsCount = this.filter?.limitResults?.();
			if (!this.filter?.groupResults?.() || typeof limitResultsCount === 'number') {
				filteredSessions.sort(this.sorter.compare.bind(this.sorter));
			}

			// Apply limiter if configured (requires sorting)
			if (typeof limitResultsCount === 'number') {
				filteredSessions = filteredSessions.slice(0, limitResultsCount);
			}

			// Callback results count
			this.filter?.notifyResults?.(filteredSessions.length);
			this._onDidGetChildren.fire(filteredSessions.length);

			// Group sessions into sections if enabled
			if (this.filter?.groupResults?.()) {
				return this.groupSessionsIntoSections(filteredSessions);
			}

			// Otherwise return flat sorted list
			return filteredSessions;
		}

		// Sessions	section
		else if (isAgentSessionSection(element)) {
			return element.sessions;
		}

		// Session element
		else {
			return [];
		}
	}

	private groupSessionsIntoSections(sessions: IAgentSession[]): AgentSessionListItem[] {
		const sortedSessions = sessions.sort(this.sorter.compare.bind(this.sorter));

		if (this.filter?.groupResults?.() === AgentSessionsGrouping.Capped) {
			if (this.filter?.getExcludes().read) {
				return sortedSessions; // When filtering to show only unread sessions, show a flat list
			}

			return this.groupSessionsCapped(sortedSessions);
		} else {
			return this.groupSessionsByDate(sortedSessions);
		}
	}

	private groupSessionsCapped(sortedSessions: IAgentSession[]): AgentSessionListItem[] {
		const result: AgentSessionListItem[] = [];

		const firstArchivedIndex = sortedSessions.findIndex(session => session.isArchived());
		const nonArchivedCount = firstArchivedIndex === -1 ? sortedSessions.length : firstArchivedIndex;

		const topSessions = sortedSessions.slice(0, Math.min(AgentSessionsDataSource.CAPPED_SESSIONS_LIMIT, nonArchivedCount));
		const othersSessions = sortedSessions.slice(topSessions.length);

		// Add top sessions directly (no section header)
		result.push(...topSessions);

		// Add "More" section for the rest
		if (othersSessions.length > 0) {
			result.push({
				section: AgentSessionSection.More,
				label: AgentSessionSectionLabels[AgentSessionSection.More],
				sessions: othersSessions
			});
		}

		return result;
	}

	private groupSessionsByDate(sortedSessions: IAgentSession[]): AgentSessionListItem[] {
		const result: AgentSessionListItem[] = [];
		const groupedSessions = groupAgentSessionsByDate(sortedSessions);

		for (const { sessions, section, label } of groupedSessions.values()) {
			if (sessions.length === 0) {
				continue;
			}

			result.push({ section, label, sessions });
		}

		return result;
	}
}

export const AgentSessionSectionLabels = {
	[AgentSessionSection.Today]: localize('agentSessions.todaySection', "Today"),
	[AgentSessionSection.Yesterday]: localize('agentSessions.yesterdaySection', "Yesterday"),
	[AgentSessionSection.Week]: localize('agentSessions.weekSection', "Last 7 days"),
	[AgentSessionSection.Older]: localize('agentSessions.olderSection', "Older"),
	[AgentSessionSection.Archived]: localize('agentSessions.archivedSection', "Archived"),
	[AgentSessionSection.More]: localize('agentSessions.moreSection', "More"),
};

const DAY_THRESHOLD = 24 * 60 * 60 * 1000;
const WEEK_THRESHOLD = 7 * DAY_THRESHOLD;

export function groupAgentSessionsByDate(sessions: IAgentSession[]): Map<AgentSessionSection, IAgentSessionSection> {
	const now = Date.now();
	const startOfToday = new Date(now).setHours(0, 0, 0, 0);
	const startOfYesterday = startOfToday - DAY_THRESHOLD;
	const weekThreshold = now - WEEK_THRESHOLD;

	const todaySessions: IAgentSession[] = [];
	const yesterdaySessions: IAgentSession[] = [];
	const weekSessions: IAgentSession[] = [];
	const olderSessions: IAgentSession[] = [];
	const archivedSessions: IAgentSession[] = [];

	for (const session of sessions) {
		if (session.isArchived()) {
			archivedSessions.push(session);
		} else {
			const sessionTime = getAgentSessionTime(session.timing);
			if (sessionTime >= startOfToday) {
				todaySessions.push(session);
			} else if (sessionTime >= startOfYesterday) {
				yesterdaySessions.push(session);
			} else if (sessionTime >= weekThreshold) {
				weekSessions.push(session);
			} else {
				olderSessions.push(session);
			}
		}
	}

	return new Map<AgentSessionSection, IAgentSessionSection>([
		[AgentSessionSection.Today, { section: AgentSessionSection.Today, label: AgentSessionSectionLabels[AgentSessionSection.Today], sessions: todaySessions }],
		[AgentSessionSection.Yesterday, { section: AgentSessionSection.Yesterday, label: AgentSessionSectionLabels[AgentSessionSection.Yesterday], sessions: yesterdaySessions }],
		[AgentSessionSection.Week, { section: AgentSessionSection.Week, label: AgentSessionSectionLabels[AgentSessionSection.Week], sessions: weekSessions }],
		[AgentSessionSection.Older, { section: AgentSessionSection.Older, label: AgentSessionSectionLabels[AgentSessionSection.Older], sessions: olderSessions }],
		[AgentSessionSection.Archived, { section: AgentSessionSection.Archived, label: AgentSessionSectionLabels[AgentSessionSection.Archived], sessions: archivedSessions }],
	]);
}

export function sessionDateFromNow(sessionTime: number): string {
	const now = Date.now();
	const startOfToday = new Date(now).setHours(0, 0, 0, 0);
	const startOfYesterday = startOfToday - DAY_THRESHOLD;
	const startOfTwoDaysAgo = startOfYesterday - DAY_THRESHOLD;

	// our grouping by date uses absolute start times for "Today"
	// and "Yesterday" while `fromNow` only works with full 24h
	// and 48h ranges for these. To prevent a label like "1 day ago"
	// to show under the "Last 7 Days" section, we do a bit of
	// normalization logic.

	if (sessionTime < startOfToday && sessionTime >= startOfYesterday) {
		return localize('date.fromNow.days.singular', '1 day');
	}

	if (sessionTime < startOfYesterday && sessionTime >= startOfTwoDaysAgo) {
		return localize('date.fromNow.days.multiple', '2 days');
	}

	return fromNow(sessionTime, false);
}

export class AgentSessionsIdentityProvider implements IIdentityProvider<IAgentSessionsModel | AgentSessionListItem> {

	getId(element: IAgentSessionsModel | AgentSessionListItem): string {
		if (isAgentSessionSection(element)) {
			return `section-${element.section}`;
		}

		if (isAgentSession(element)) {
			return element.resource.toString();
		}

		return 'agent-sessions-id';
	}

	getGroupId(element: IAgentSessionsModel | AgentSessionListItem): number | NotSelectableGroupIdType {
		if (isAgentSessionSection(element) || isAgentSessionsModel(element)) {
			return NotSelectableGroupId;
		}
		return 1;
	}
}

export class AgentSessionsCompressionDelegate implements ITreeCompressionDelegate<AgentSessionListItem> {

	isIncompressible(element: AgentSessionListItem): boolean {
		return true;
	}
}

export interface IAgentSessionsSorterOptions {
	overrideCompare?(sessionA: IAgentSession, sessionB: IAgentSession): number | undefined;
}

export class AgentSessionsSorter implements ITreeSorter<IAgentSession> {

	constructor(private readonly options?: IAgentSessionsSorterOptions) { }

	compare(sessionA: IAgentSession, sessionB: IAgentSession): number {

		// Input Needed
		const aNeedsInput = sessionA.status === AgentSessionStatus.NeedsInput;
		const bNeedsInput = sessionB.status === AgentSessionStatus.NeedsInput;

		if (aNeedsInput && !bNeedsInput) {
			return -1; // a (needs input) comes before b (other)
		}
		if (!aNeedsInput && bNeedsInput) {
			return 1; // a (other) comes after b (needs input)
		}

		// Archived
		const aArchived = sessionA.isArchived();
		const bArchived = sessionB.isArchived();

		if (!aArchived && bArchived) {
			return -1; // a (non-archived) comes before b (archived)
		}
		if (aArchived && !bArchived) {
			return 1; // a (archived) comes after b (non-archived)
		}

		// Before we compare by time, allow override
		const override = this.options?.overrideCompare?.(sessionA, sessionB);
		if (typeof override === 'number') {
			return override;
		}

		// Sort by end or start time (most recent first)
		const timeA = getAgentSessionTime(sessionA.timing);
		const timeB = getAgentSessionTime(sessionB.timing);
		return timeB - timeA;
	}
}

export class AgentSessionsKeyboardNavigationLabelProvider implements ICompressibleKeyboardNavigationLabelProvider<AgentSessionListItem> {

	getKeyboardNavigationLabel(element: AgentSessionListItem): string {
		if (isAgentSessionSection(element)) {
			return element.label;
		}

		return element.label;
	}

	getCompressedNodeKeyboardNavigationLabel(elements: AgentSessionListItem[]): { toString(): string | undefined } | undefined {
		return undefined; // not enabled
	}
}

export class AgentSessionsDragAndDrop extends Disposable implements ITreeDragAndDrop<AgentSessionListItem> {

	constructor(
		@IInstantiationService private readonly instantiationService: IInstantiationService
	) {
		super();
	}

	onDragStart(data: IDragAndDropData, originalEvent: DragEvent): void {
		const elements = (data.getData() as AgentSessionListItem[]).filter(e => isAgentSession(e));
		const uris = coalesce(elements.map(e => e.resource));
		this.instantiationService.invokeFunction(accessor => fillEditorsDragData(accessor, uris, originalEvent));
	}

	getDragURI(element: AgentSessionListItem): string | null {
		if (isAgentSessionSection(element)) {
			return null; // section headers are not draggable
		}

		return element.resource.toString();
	}

	getDragLabel?(elements: AgentSessionListItem[], originalEvent: DragEvent): string | undefined {
		const sessions = elements.filter(e => isAgentSession(e));
		if (sessions.length === 1) {
			return sessions[0].label;
		}

		return localize('agentSessions.dragLabel', "{0} agent sessions", sessions.length);
	}

	onDragOver(data: IDragAndDropData, targetElement: AgentSessionListItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): boolean | ITreeDragOverReaction {
		return false;
	}

	drop(data: IDragAndDropData, targetElement: AgentSessionListItem | undefined, targetIndex: number | undefined, targetSector: ListViewTargetSector | undefined, originalEvent: DragEvent): void { }
}
