import { SignalWatcher } from '@lit-labs/signals';
import { consume } from '@lit/context';
import type { PropertyValues, TemplateResult } from 'lit';
import { css, html, nothing } from 'lit';
import { customElement, property, state } from 'lit/decorators.js';
import { when } from 'lit/directives/when.js';
import { formatDate, fromNow } from '@gitlens/utils/date.js';
import { interpolate } from '@gitlens/utils/string.js';
import type { GlWebviewCommandsOrCommandsWithSuffix } from '../../../../../constants.commands.js';
import { isSubscriptionTrialOrPaidFromState } from '../../../../../plus/gk/utils/subscription.utils.js';
import type { LaunchpadCommandArgs } from '../../../../../plus/launchpad/launchpad.js';
import {
	actionGroupMap,
	launchpadCategoryToGroupMap,
	launchpadGroupIconMap,
	launchpadGroupLabelMap,
} from '../../../../../plus/launchpad/models/launchpad.js';
import { createCommandLink } from '../../../../../system/commands.js';
import type {
	AgentSessionState,
	BranchRef,
	CreatePullRequestCommandArgs,
	GetOverviewBranch,
	OpenInGraphParams,
	OpenInTimelineParams,
	OpenWorktreeCommandArgs,
} from '../../../../home/protocol.js';
import type { HomeState } from '../../../home/state.js';
import { homeStateContext } from '../../../home/state.js';
import { agentPhaseToCategory, matchAgentSessionsForWorktree } from '../../../shared/agentUtils.js';
import type { GlCard } from '../../../shared/components/card/card.js';
import { getWipTooltipParts } from '../../../shared/components/commit/wip-stats.js';
import { GlElement, observe } from '../../../shared/components/element.js';
import { srOnlyStyles } from '../../../shared/components/styles/lit/a11y.css.js';
import type { AIContextState } from '../../../shared/contexts/ai.js';
import { aiContext } from '../../../shared/contexts/ai.js';
import type { SubscriptionContextState } from '../../../shared/contexts/subscription.js';
import { subscriptionContext } from '../../../shared/contexts/subscription.js';
import type { WebviewContext } from '../../../shared/contexts/webview.js';
import { webviewContext } from '../../../shared/contexts/webview.js';
import { linkStyles } from '../../shared/components/vscode.css.js';
import '../../../shared/components/code-icon.js';
import '../../../shared/components/avatar/avatar.js';
import '../../../shared/components/avatar/avatar-list.js';
import '../../../shared/components/commit/commit-stats.js';
import '../../../shared/components/formatted-date.js';
import '../../../shared/components/overlays/tooltip.js';
import '../../../shared/components/pills/tracking-status.js';
import '../../../shared/components/rich/issue-icon.js';
import '../../../shared/components/rich/pr-icon.js';
import '../../../shared/components/actions/action-item.js';
import '../../../shared/components/actions/action-nav.js';
import '../../../shared/components/branch-icon.js';
import '../../shared/components/merge-target-status.js';
import '../../../shared/components/pills/agent-status-pill.js';

export const branchCardStyles = css`
	* {
		box-sizing: border-box;
	}

	gl-avatar-list {
		--gl-avatar-size: 2.4rem;

		margin-block: -0.4rem;
	}

	.branch-item {
		position: relative;
	}

	.branch-item__container {
		display: flex;
		flex-direction: column;
		gap: var(--gl-space-6);
	}

	.branch-item__container > * {
		margin-block: 0;
	}

	.branch-item__section {
		display: flex;
		flex-direction: column;
		gap: var(--gl-space-4);
	}

	.branch-item__section > * {
		margin-block: 0;
	}

	.branch-item__section--details {
		font-size: 0.9em;
		color: var(--vscode-descriptionForeground);
	}

	.branch-item__actions {
		display: flex;
		flex-direction: row;
		gap: var(--gl-space-8);
		align-items: center;
		justify-content: flex-end;
		font-size: 0.9em;
	}

	/* :empty selector doesn't work with lit */
	.branch-item__actions:not(:has(*)) {
		display: none;
	}

	.branch-item__icon {
		flex: none;
		color: var(--vscode-descriptionForeground);
	}

	.branch-item__name {
		flex-grow: 1;
		overflow: hidden;
		text-overflow: ellipsis;
		font-weight: bold;
		white-space: nowrap;
	}

	.branch-item__name--secondary {
		font-weight: normal;
	}

	.branch-item__identifier {
		color: var(--vscode-descriptionForeground);
		text-decoration: none;
	}

	.branch-item__grouping {
		display: inline-flex;
		gap: var(--gl-space-6);
		align-items: center;
		max-width: 100%;
		margin-block: 0;
	}

	.branch-item__agents {
		display: flex;
		flex-flow: row wrap;
		gap: var(--gl-space-4);
		align-items: center;
	}

	.branch-item__agents code-icon {
		color: var(--vscode-descriptionForeground);
	}

	.branch-item__changes {
		display: flex;
		flex-wrap: wrap;
		gap: var(--gl-space-10);
		align-items: center;
		justify-content: flex-end;
		white-space: nowrap;
	}

	.branch-item__date {
		margin-inline-end: auto;
	}

	.branch-item__summary {
		display: flex;
		gap: var(--gl-space-6);
		align-items: center;
	}

	.branch-item__collapsed-actions {
		position: absolute;
		right: 0.4rem;
		bottom: 0.3rem;
		z-index: var(--gl-branch-card-actions-zindex, 2);
		padding: var(--gl-space-4) var(--gl-space-6);
		background-color: var(--gl-card-hover-background);
	}

	.branch-item:not(:focus-within, :hover) .branch-item__collapsed-actions {
		${srOnlyStyles}
	}

	.work-item {
		--gl-card-background: color-mix(in lab, var(--vscode-sideBar-background) 100%, #fff 3%);
		--gl-card-hover-background: color-mix(in lab, var(--vscode-sideBar-background) 100%, #fff 1.5%);
	}

	.work-item::part(base) {
		margin-block-end: 0;
	}

	.branch-item__section.mb-1 {
		margin-block: var(--gl-space-4);
	}

	.branch-item__merge-target {
		margin-inline-end: auto;
	}

	.branch-item__row {
		display: flex;
		gap: var(--gl-space-8);
	}

	.branch-item__row [full] {
		flex-grow: 1;
	}

	.branch-item__missing {
		--button-foreground: inherit;
	}

	:host-context(.vscode-dark) .branch-item__missing,
	:host-context(.vscode-high-contrast) .branch-item__missing {
		--button-background: color-mix(in lab, var(--vscode-sideBar-background) 100%, #fff 3%);
		--button-hover-background: color-mix(in lab, var(--vscode-sideBar-background) 100%, #fff 1.5%);
		--button-border: color-mix(in lab, var(--vscode-sideBar-background) 100%, #fff 12%);
	}

	:host-context(.vscode-light) .branch-item__missing,
	:host-context(.vscode-high-contrast-light) .branch-item__missing {
		--button-background: color-mix(in lab, var(--vscode-sideBar-background) 100%, #000 8%);
		--button-hover-background: color-mix(in lab, var(--vscode-sideBar-background) 100%, #000 10%);
		--button-border: color-mix(in lab, var(--vscode-sideBar-background) 100%, #000 14%);
	}

	.branch-item__category {
		margin-inline-start: var(--gl-space-6);
	}

	.launchpad-grouping--mergeable {
		color: var(--vscode-gitlens-launchpadIndicatorMergeableColor);
	}

	.launchpad-grouping--blocked {
		color: var(--vscode-gitlens-launchpadIndicatorBlockedColor);
	}

	.launchpad-grouping--attention {
		color: var(--vscode-gitlens-launchpadIndicatorAttentionColor);
	}

	.wip__pill {
		display: flex;
		flex-direction: row;
		gap: var(--gl-space-10);
	}

	.wip__tooltip {
		display: contents;
		vertical-align: middle;
	}

	.wip__tooltip p {
		margin-block: 0;
	}
`;

type NothingType = typeof nothing;

declare global {
	interface GlobalEventHandlersEventMap {
		'gl-branch-card-expand-toggled': CustomEvent<{ expanded: boolean }>;
	}
}

// Cast needed because SignalWatcher's Constructor<T> type excludes abstract classes,
// but GlBranchCardBase is abstract itself and only concrete subclasses are instantiated.
const SignalWatcherGlElement = SignalWatcher(
	GlElement as unknown as new (...args: any[]) => GlElement,
) as unknown as typeof GlElement;

export abstract class GlBranchCardBase extends SignalWatcherGlElement {
	static override styles = [linkStyles, branchCardStyles];

	@consume({ context: subscriptionContext, subscribe: true })
	protected _subscription!: SubscriptionContextState;

	@consume({ context: aiContext })
	protected _aiCtx!: AIContextState;

	@consume({ context: webviewContext })
	protected _webview!: WebviewContext;

	@consume({ context: homeStateContext })
	protected _homeState!: HomeState;

	@property()
	repo!: string;

	@property({ type: Boolean })
	showUpgrade = false;

	private _branch!: GetOverviewBranch;
	get branch(): GetOverviewBranch {
		return this._branch;
	}
	@property({ type: Object })
	set branch(value: GetOverviewBranch) {
		this._branch = value;
		this.autolinksPromise = value?.autolinks;
		this.contributorsPromise = value?.contributors;
		this.issuesPromise = value?.issues;
		this.prPromise = value?.pr;
		this.mergeTargetPromise = value?.mergeTarget;
		this.remotePromise = value?.remote;
		this.wipPromise = value?.wip;
	}

	@state()
	private _autolinks!: Awaited<GetOverviewBranch['autolinks']>;
	get autolinks(): Awaited<GetOverviewBranch['autolinks']> {
		return this._autolinks;
	}

	private _autolinksPromise!: GetOverviewBranch['autolinks'];
	get autolinksPromise(): GetOverviewBranch['autolinks'] {
		return this._autolinksPromise;
	}
	set autolinksPromise(value: GetOverviewBranch['autolinks']) {
		if (this._autolinksPromise === value) return;

		this._autolinksPromise = value;
		void this._autolinksPromise?.then(
			r => (this._autolinks = r),
			() => (this._autolinks = undefined),
		);
	}

	@state()
	private _contributors!: Awaited<GetOverviewBranch['contributors']>;
	get contributors(): Awaited<GetOverviewBranch['contributors']> {
		return this._contributors;
	}

	private _contributorsPromise!: GetOverviewBranch['contributors'];
	get contributorsPromise(): GetOverviewBranch['contributors'] {
		return this._contributorsPromise;
	}
	set contributorsPromise(value: GetOverviewBranch['contributors']) {
		if (this._contributorsPromise === value) return;

		this._contributorsPromise = value;
		void this._contributorsPromise?.then(
			r => (this._contributors = r),
			() => (this._contributors = undefined),
		);
	}

	@state()
	private _issues!: Awaited<GetOverviewBranch['issues']>;
	get issues(): Awaited<GetOverviewBranch['issues']> {
		return this._issues;
	}

	private _issuesPromise!: GetOverviewBranch['issues'];
	get issuesPromise(): GetOverviewBranch['issues'] {
		return this._issuesPromise;
	}
	set issuesPromise(value: GetOverviewBranch['issues']) {
		if (this._issuesPromise === value) return;

		this._issuesPromise = value;
		void this._issuesPromise?.then(
			r => (this._issues = r),
			() => (this._issues = undefined),
		);
	}

	@state()
	private _pr!: Awaited<GetOverviewBranch['pr']>;
	get pr(): Awaited<GetOverviewBranch['pr']> {
		return this._pr;
	}

	private _prPromise!: GetOverviewBranch['pr'];
	get prPromise(): GetOverviewBranch['pr'] {
		return this._prPromise;
	}
	set prPromise(value: GetOverviewBranch['pr']) {
		if (this._prPromise === value) return;

		this._prPromise = value;
		void this._prPromise?.then(
			r => {
				this._pr = r;
				this.launchpadItemPromise = r?.launchpad;
			},
			() => {
				this._pr = undefined;
				this.launchpadItemPromise = undefined;
			},
		);
	}

	@state()
	private _launchpadItem!: Awaited<NonNullable<Awaited<GetOverviewBranch['pr']>>['launchpad']>;
	get launchpadItem(): Awaited<NonNullable<Awaited<GetOverviewBranch['pr']>>['launchpad']> {
		return this._launchpadItem;
	}

	private _launchpadItemPromise!: NonNullable<Awaited<GetOverviewBranch['pr']>>['launchpad'];
	get launchpadItemPromise(): NonNullable<Awaited<GetOverviewBranch['pr']>>['launchpad'] {
		return this._launchpadItemPromise;
	}
	set launchpadItemPromise(value: NonNullable<Awaited<GetOverviewBranch['pr']>>['launchpad']) {
		if (this._launchpadItemPromise === value) return;

		this._launchpadItemPromise = value;
		void this._launchpadItemPromise?.then(
			r => (this._launchpadItem = r),
			() => (this._launchpadItem = undefined),
		);
	}

	@state()
	private _mergeTarget!: Awaited<GetOverviewBranch['mergeTarget']>;
	get mergeTarget(): Awaited<GetOverviewBranch['mergeTarget']> {
		return this._mergeTarget;
	}

	private _mergeTargetPromise!: GetOverviewBranch['mergeTarget'];
	get mergeTargetPromise(): GetOverviewBranch['mergeTarget'] {
		return this._mergeTargetPromise;
	}
	set mergeTargetPromise(value: GetOverviewBranch['mergeTarget']) {
		if (this._mergeTargetPromise === value) return;

		this._mergeTargetPromise = value;
		void this._mergeTargetPromise?.then(
			r => (this._mergeTarget = r),
			() => (this._mergeTarget = undefined),
		);
	}

	@state()
	private _remote!: Awaited<GetOverviewBranch['remote']>;
	get remote(): Awaited<GetOverviewBranch['remote']> {
		return this._remote;
	}

	private _remotePromise!: GetOverviewBranch['remote'];
	get remotePromise(): GetOverviewBranch['remote'] {
		return this._remotePromise;
	}
	set remotePromise(value: GetOverviewBranch['remote']) {
		if (this._remotePromise === value) return;

		this._remotePromise = value;
		void this._remotePromise?.then(
			r => (this._remote = r),
			() => (this._remote = undefined),
		);
	}

	@state()
	private _wip!: Awaited<GetOverviewBranch['wip']>;
	get wip(): Awaited<GetOverviewBranch['wip']> {
		return this._wip;
	}

	private _wipPromise!: GetOverviewBranch['wip'];
	get wipPromise(): GetOverviewBranch['wip'] {
		return this._wipPromise;
	}
	set wipPromise(value: GetOverviewBranch['wip']) {
		if (this._wipPromise === value) return;

		this._wipPromise = value;
		void this._wipPromise?.then(
			r => (this._wip = r),
			() => (this._wip = undefined),
		);
	}

	@property({ type: Boolean, reflect: true })
	busy = false;

	@property({ type: Boolean, reflect: true })
	expanded = false;

	@property({ type: Boolean, reflect: true })
	expandable = false;

	private eventController?: AbortController;

	@observe('expandable')
	private onExpandableChanged() {
		this.attachFocusListener();
	}

	get branchRef(): BranchRef {
		return {
			repoPath: this.repo,
			branchId: this.branch.id,
			branchName: this.branch.name,
			worktree: this.branch.worktree
				? { name: this.branch.worktree.name, isDefault: this.branch.worktree.isDefault }
				: undefined,
		};
	}

	get isWorktree(): boolean {
		return this.branch.worktree != null;
	}

	get cardIndicator(): GlCard['indicator'] {
		return getLaunchpadItemGrouping(getLaunchpadItemGroup(this.pr, this.launchpadItem)) ?? 'base';
	}

	get branchCardIndicator(): GlCard['indicator'] {
		if (!this.branch.opened) return undefined;

		if (this.wip?.pausedOpStatus != null) {
			if (this.wip?.hasConflicts) return 'conflict';

			switch (this.wip.pausedOpStatus.type) {
				case 'cherry-pick':
					return 'cherry-picking';
				case 'merge':
					return 'merging';
				case 'rebase':
					return 'rebasing';
				case 'revert':
					return 'reverting';
			}
		}

		const hasWip =
			this.wip?.workingTreeState != null &&
			this.wip.workingTreeState.added + this.wip.workingTreeState.changed + this.wip.workingTreeState.deleted > 0;

		if (hasWip) {
			return 'branch-changes';
		}

		if (this.mergeTarget?.mergedStatus?.merged) {
			return 'branch-merged';
		}

		switch (this.branch.status) {
			case 'ahead':
				return 'branch-ahead';
			case 'behind':
				return 'branch-behind';
			case 'diverged':
				return 'branch-diverged';
			case 'upToDate':
				return 'branch-synced';
			case 'missingUpstream':
				return 'branch-missingUpstream';
			default:
				return undefined;
		}
	}

	override connectedCallback(): void {
		super.connectedCallback?.();
		this.attachFocusListener();
	}

	override disconnectedCallback(): void {
		super.disconnectedCallback?.();
		this.eventController?.abort();
	}

	private attachFocusListener() {
		this.eventController?.abort();
		this.eventController = undefined;
		if (this.expandable) {
			this.eventController ??= new AbortController();
			this.addEventListener('focusin', this.onFocus, { signal: this.eventController.signal });
		}
	}

	private readonly onFocus = (e: FocusEvent) => {
		const actionElement = e.composedPath().some(el => (el as HTMLElement).matches?.('action-item') ?? false);
		if (actionElement || this.expanded) return;

		this.toggleExpanded(true);
	};

	protected renderIssues(): TemplateResult | NothingType {
		const { autolinks, issues } = this;
		const issuesSource = issues?.length ? issues : autolinks;
		if (!issuesSource?.length) return nothing;

		return html`
			${issuesSource.map(issue => {
				return html`
					<p class="branch-item__grouping">
						<span class="branch-item__icon">
							<issue-icon state=${issue.state} issue-id=${issue.id}></issue-icon>
						</span>
						<a href=${issue.url} class="branch-item__name branch-item__name--secondary">${issue.title}</a>
						<span class="branch-item__identifier">${isNaN(parseInt(issue.id)) ? '' : '#'}${issue.id}</span>
					</p>
				`;
			})}
		`;
	}

	protected renderWip(): TemplateResult | NothingType {
		const workingTreeState = this.wip?.workingTreeState;
		if (workingTreeState == null) return nothing;

		const parts = getWipTooltipParts(workingTreeState);

		return html`<gl-tooltip class="wip__pill" placement="bottom"
			><commit-stats
				added=${workingTreeState.added}
				modified=${workingTreeState.changed}
				removed=${workingTreeState.deleted}
				symbol="icons"
				no-tooltip
			></commit-stats>
			<span class="wip__tooltip" slot="content">
				<p>${parts.length ? `${parts.join(', ')} in the working tree` : 'No working tree changes'}</p>
			</span>
		</gl-tooltip>`;
	}

	protected renderAvatars(): TemplateResult | NothingType {
		const { contributors } = this;

		if (!contributors?.length) return nothing;

		return html`<gl-avatar-list
			.avatars=${contributors.map(a => ({ name: a.name, src: a.avatarUrl }))}
			max="1"
		></gl-avatar-list>`;
	}

	protected renderTracking(showWip = false): TemplateResult | NothingType {
		const upstream = this.branch.upstream;
		if (upstream == null) return nothing;

		let working = 0;
		let wipExtra: TemplateResult | NothingType = nothing;
		if (showWip) {
			const workingTreeState = this.wip?.workingTreeState;
			if (workingTreeState != null) {
				working = workingTreeState.added + workingTreeState.changed + workingTreeState.deleted;

				const wipParts = getWipTooltipParts(workingTreeState);
				if (wipParts.length) {
					wipExtra = html`<p slot="extra">${wipParts.join(', ')} in the working tree</p>`;
				}
			}
		}

		return html`<gl-tracking-status
			.branchName=${this.branch.name}
			.upstreamName=${upstream.name}
			.missingUpstream=${upstream.missing ?? false}
			.ahead=${upstream.state.ahead}
			.behind=${upstream.state.behind}
			.working=${working}
			colorized
			outlined
			>${wipExtra}</gl-tracking-status
		>`;
	}

	protected abstract getBranchActions(): TemplateResult[];
	protected renderBranchActions(): TemplateResult | NothingType {
		const actions = this.getBranchActions?.();
		if (!actions?.length) return nothing;

		return html`<action-nav>${actions}</action-nav>`;
	}

	protected abstract getPrActions(): TemplateResult[];
	protected renderPrActions(): TemplateResult | NothingType {
		const actions = this.getPrActions?.();
		if (!actions?.length) return nothing;

		return html`<action-nav>${actions}</action-nav>`;
	}

	protected abstract getCollapsedActions(): TemplateResult[];
	protected renderCollapsedActions(): TemplateResult | NothingType {
		if (this.expanded) return nothing;

		const actions = this.getCollapsedActions?.();
		if (!actions?.length) return nothing;

		return html`<action-nav class="branch-item__collapsed-actions">${actions}</action-nav>`;
	}

	protected createWebviewCommandLinkWithBranchRef<T>(
		command: GlWebviewCommandsOrCommandsWithSuffix,
		args?: Omit<T, keyof BranchRef>,
	): string {
		return this._webview.createCommandLink<T | BranchRef>(
			command,
			args ? { ...args, ...this.branchRef } : this.branchRef,
		);
	}

	protected renderTimestamp(): TemplateResult | NothingType {
		const { timestamps } = this.branch;
		if (timestamps == null) return nothing;

		const { lastCommit, lastAccessed, lastModified } = timestamps;

		// Compute effective date (most recent)
		const effectiveTimestamp = Math.max(lastCommit ?? 0, lastAccessed ?? 0, lastModified ?? 0);
		if (effectiveTimestamp === 0) return nothing;

		// Determine which date is the most recent for labeling (bias modified over accessed)
		let effectiveLabel: string;
		if (lastModified != null && lastModified >= (lastAccessed ?? 0) && lastModified >= (lastCommit ?? 0)) {
			effectiveLabel = 'Modified';
		} else if (lastAccessed != null && lastAccessed >= (lastModified ?? 0) && lastAccessed >= (lastCommit ?? 0)) {
			effectiveLabel = 'Accessed';
		} else {
			effectiveLabel = 'Committed';
		}

		// Build tooltip lines in fixed order: modified, accessed, committed
		// Collapse accessed+modified into just "Modified" when they're within the same second
		const dateFormat = 'MMMM Do, YYYY h:mma'; // TODO fix this
		const fmtLine = (label: string, ts: number) =>
			html`${label} ${fromNow(new Date(ts))} <i>(${formatDate(new Date(ts), dateFormat)})</i>`;
		const sameAccessedModified =
			lastAccessed != null && lastModified != null && Math.abs(lastAccessed - lastModified) < 30000;
		const tooltipLines: TemplateResult[] = [];
		if (sameAccessedModified) {
			tooltipLines.push(fmtLine('Modified', lastModified));
		} else {
			if (lastAccessed != null) {
				tooltipLines.push(fmtLine('Accessed', lastAccessed));
			}
			if (lastModified != null) {
				tooltipLines.push(fmtLine('Modified', lastModified));
			}
		}
		if (lastCommit != null) {
			tooltipLines.push(fmtLine('Committed', lastCommit));
		}

		const effectiveDate = new Date(effectiveTimestamp);
		return html`<gl-tooltip class="branch-item__date">
			<time datetime="${effectiveDate.toISOString()}">${effectiveLabel} ${fromNow(effectiveDate)}</time>
			<span slot="content">${tooltipLines.map((line, i) => (i > 0 ? html`<br />${line}` : line))}</span>
		</gl-tooltip>`;
	}

	protected abstract renderBranchIndicator?(): TemplateResult | undefined;

	protected renderBranchItem(actionsSection?: TemplateResult | NothingType): TemplateResult | NothingType {
		const wip = this.renderWip();
		const tracking = this.renderTracking();
		const avatars = this.renderAvatars();
		const indicator = this.branch.opened ? undefined : this.renderBranchIndicator?.();
		const mergeTargetStatus = this.renderMergeTargetStatus();
		const timestamp = this.renderTimestamp();

		return html`
			<gl-work-item
				?primary=${!this.branch.opened}
				?nested=${!this.branch.opened}
				.indicator=${this.branchCardIndicator}
				?expanded=${this.expanded}
			>
				<div class="branch-item__section">
					<p class="branch-item__grouping">
						<span class="branch-item__icon"> ${this.renderBranchIcon()} </span>
						<span class="branch-item__name">${this.branch.name}</span>
					</p>
				</div>
				${when(
					timestamp || indicator || wip || tracking || avatars,
					() => html`
						<div class="branch-item__section branch-item__section--details" slot="context">
							<p class="branch-item__changes">${timestamp}${indicator}${wip}${tracking}${avatars}</p>
						</div>
					`,
				)}
				${this.renderAgentPillsRow()}
				${when(
					// TODO: this doesn't work properly. nothing is true, empty html template is true
					actionsSection || mergeTargetStatus,
					() =>
						html`<div class="branch-item__actions" slot="actions">
							${mergeTargetStatus ?? nothing}${actionsSection ?? nothing}
						</div>`,
				)}
				<span class="branch-item__summary" slot="summary">${this.renderTracking(true)} ${avatars}</span>
			</gl-work-item>
		`;
	}

	private renderBranchIcon() {
		const hasChanges =
			this.wip?.workingTreeState != null &&
			this.wip.workingTreeState.added + this.wip.workingTreeState.changed + this.wip.workingTreeState.deleted > 0;
		return html`<gl-branch-icon
			branch="${this.branch.name}"
			status="${this.branch.status}"
			?hasChanges=${hasChanges}
			upstream=${this.branch.upstream?.name}
			?worktree=${this.branch.worktree != null}
			?is-default=${this.branch.worktree?.isDefault ?? false}
		></gl-branch-icon>`;
	}

	private renderAgentPillsRow(): TemplateResult | NothingType {
		// Home's `branch.worktree == null` means "not checked out anywhere" — never the default
		// worktree (which surfaces with `worktree.path === repoPath`). Bailing here avoids the
		// matcher's `worktreePath ?? repoPath` fallback, which exists for the Graph's "undefined
		// ≡ default" convention and would otherwise false-match every no-worktree branch to the
		// default-worktree session.
		const worktreePath = this.branch.worktree?.path;
		if (worktreePath == null) return nothing;

		const sessions = matchAgentSessionsForWorktree(this._homeState?.agentSessions?.get(), {
			repoPath: this.repo,
			worktreePath: worktreePath,
		});
		if (sessions == null || sessions.length === 0) return nothing;

		if (this.expanded) {
			return html`
				<div class="branch-item__agents">
					<code-icon icon="robot"></code-icon>
					${sessions.map(s => html`<gl-agent-status-pill .session=${s}></gl-agent-status-pill>`)}
				</div>
			`;
		}

		// Actionable (`needs-input`) sessions never aggregate — each one needs its own Allow/Deny
		// popover. Working and idle sessions collapse into one summary pill per category.
		const needsInput: AgentSessionState[] = [];
		const working: AgentSessionState[] = [];
		const idle: AgentSessionState[] = [];
		for (const s of sessions) {
			const cat = agentPhaseToCategory[s.phase];
			if (cat === 'needs-input') {
				needsInput.push(s);
			} else if (cat === 'working') {
				working.push(s);
			} else {
				idle.push(s);
			}
		}

		return html`
			<div class="branch-item__agents">
				<code-icon icon="robot"></code-icon>
				${needsInput.map(s => html`<gl-agent-status-pill .session=${s}></gl-agent-status-pill>`)}
				${working.length > 0
					? html`<gl-agent-status-pill
							.summary=${{ category: 'working', sessions: working }}
						></gl-agent-status-pill>`
					: nothing}
				${idle.length > 0
					? html`<gl-agent-status-pill
							.summary=${{ category: 'idle', sessions: idle }}
						></gl-agent-status-pill>`
					: nothing}
			</div>
		`;
	}

	protected renderPrItem(): TemplateResult | NothingType {
		if (!this.pr) {
			if (this.branch.upstream?.missing === false && this.expanded) {
				return html`
					<div>
						<button-container grouping="split" layout="full">
							<gl-button
								class="branch-item__missing"
								appearance="secondary"
								full
								href="${this._webview.createCommandLink('gitlens.createPullRequest:', {
									ref: this.branchRef,
									describeWithAI: false,
									source: { source: 'home', detail: 'create-pr' },
								})}"
							>
								<code-icon icon="git-pull-request" slot="prefix"></code-icon>
								<span>Create a Pull Request</span>
							</gl-button>
							${this._subscription.orgSettings.get().ai &&
							this._aiCtx.state.get().enabled &&
							this.remote?.provider?.supportedFeatures?.createPullRequestWithDetails
								? html`<gl-button
										class="branch-item__missing"
										tooltip="Create a Pull Request with AI"
										appearance="secondary"
										href="${this._webview.createCommandLink<CreatePullRequestCommandArgs>(
											'gitlens.createPullRequest:',
											{
												ref: this.branchRef,
												describeWithAI: true,
												source: { source: 'home', detail: 'create-pr' },
											},
										)}"
									>
										<code-icon icon="sparkle"></code-icon>
									</gl-button>`
								: nothing}
						</button-container>
					</div>
				`;
			}
			return nothing;
		}

		const indicator: GlCard['indicator'] = this.branch.opened
			? (getLaunchpadItemGrouping(getLaunchpadItemGroup(this.pr, this.launchpadItem)) ?? 'base')
			: undefined;

		const actions = this.renderPrActions();
		return html`
			<gl-work-item ?expanded=${this.expanded} ?nested=${!this.branch.opened} .indicator=${indicator}>
				<div class="branch-item__section">
					<p class="branch-item__grouping">
						<span class="branch-item__icon">
							<pr-icon ?draft=${this.pr.draft} state=${this.pr.state} pr-id=${this.pr.id}></pr-icon>
						</span>
						<a href=${this.pr.url} class="branch-item__name branch-item__name--secondary"
							>${this.pr.title}</a
						>
						<span class="branch-item__identifier">#${this.pr.id}</span>
					</p>
				</div>
				${this.renderLaunchpadItem()}
				${when(actions != null, () => html`<div class="branch-item__actions" slot="actions">${actions}</div>`)}
			</gl-work-item>
		`;
	}

	protected renderLaunchpadItem(): TemplateResult | NothingType {
		if (this.launchpadItem == null) return nothing;

		const group = getLaunchpadItemGroup(this.pr, this.launchpadItem);
		if (group == null) return nothing;

		const groupLabel = launchpadGroupLabelMap.get(group);
		const groupIcon = launchpadGroupIconMap.get(group);

		if (groupLabel == null || groupIcon == null) return nothing;

		const groupIconString = groupIcon.match(/\$\((.*?)\)/)![1].replace('gitlens', 'gl');

		const tooltip = interpolate(actionGroupMap.get(this.launchpadItem.category)![1], {
			author: this.launchpadItem.author?.username ?? 'unknown',
			createdDateRelative: fromNow(new Date(this.launchpadItem.createdDate)),
		});

		return html`<div class="branch-item__section branch-item__section--details" slot="context">
				<p class="launchpad-grouping--${getLaunchpadItemGrouping(group)}">
					<gl-tooltip content="${tooltip}">
						<a
							href=${createCommandLink<Omit<LaunchpadCommandArgs, 'command'>>('gitlens.showLaunchpad', {
								source: 'home',
								state: { id: { uuid: this.launchpadItem.uuid, group: group } },
							} satisfies Omit<LaunchpadCommandArgs, 'command'>)}
							class="launchpad__grouping"
						>
							<code-icon icon="${groupIconString}"></code-icon
							><span class="branch-item__category">${groupLabel.toUpperCase()}</span></a
						>
					</gl-tooltip>
				</p>
			</div>
			${groupIconString
				? html`<span
						class="branch-item__summary launchpad-grouping--${getLaunchpadItemGrouping(group)}"
						slot="summary"
						><gl-tooltip placement="bottom" content="${groupLabel}"
							><code-icon icon="${groupIconString}"></code-icon></gl-tooltip
					></span>`
				: nothing}`;
	}

	protected renderMergeTargetStatus(): TemplateResult | NothingType {
		if (this.showUpgrade) {
			return html`<gl-merge-target-upgrade
				class="branch-item__merge-target"
				.state=${this._subscription.subscription.get()?.state}
			></gl-merge-target-upgrade>`;
		}

		if (!this.branch.mergeTarget) return nothing;

		return html`<gl-merge-target-status
			class="branch-item__merge-target"
			.branch=${this.branch}
			.targetPromise=${this.branch.mergeTarget}
		></gl-merge-target-status>`;
	}

	protected renderIssuesItem(): TemplateResult | NothingType {
		const issues = [...(this.issues ?? []), ...(this.autolinks ?? [])];
		if (!issues.length) {
			return nothing;
		}

		const indicator: GlCard['indicator'] = this.branch.opened ? 'base' : undefined;
		// for (const issue of issues) {
		// 	if (issue.state === 'opened') {
		// 		indicator = 'issue-open';
		// 		break;
		// 	}

		// 	if (issue.state === 'closed') {
		// 		indicator = 'issue-closed';
		// 	}
		// }

		return html`
			<gl-work-item ?expanded=${this.expanded} ?nested=${!this.branch.opened} .indicator=${indicator}>
				<div class="branch-item__section">${this.renderIssues()}</div>
			</gl-work-item>
		`;
	}

	toggleExpanded(expanded = !this.expanded): void {
		this.expanded = expanded;

		queueMicrotask(() => {
			this.emit('gl-branch-card-expand-toggled', { expanded: expanded });
		});
	}
}

@customElement('gl-branch-card')
export class GlBranchCard extends GlBranchCardBase {
	// Subclass-owned lazy merge-target state. The base's eager `_mergeTarget` stays undefined
	// here because the home overview's enrichment IPC opts out (`skipMergeTarget: true`) — this
	// mirrors the graph-overview-card hover pattern by fetching on first card expand instead.
	@state()
	private _lazyMergeTarget?: Awaited<GetOverviewBranch['mergeTarget']>;

	// True while the lazy fetch is in flight. Drives `<gl-merge-target-status loading>` so the
	// chip's `aria-busy="true"` "Checking merge target status…" affordance covers the latency
	// instead of rendering nothing until resolution.
	@state()
	private _lazyMergeTargetLoading = false;

	// In-flight/resolved promise. Handed to the chip's `targetPromise` so the loading state
	// reuses across collapse/re-expand cycles without re-firing.
	private _lazyMergeTargetPromise?: Promise<Awaited<GetOverviewBranch['mergeTarget']>>;

	// Lit's `repeat` reuses card instances when the branch list reorders, so a `branch` prop
	// transition can swap us onto a different branch entirely. Track the id we last fetched
	// for so `willUpdate` can drop stale state on transition.
	private _lazyMergeTargetFetchedFor?: string;

	/**
	 * Surface the lazy-fetched merge-target through the base's `mergeTarget` getter so the
	 * base's `branchCardIndicator` automatically returns `'branch-merged'` after resolve — no
	 * separate indicator override needed. Lit re-renders on `_lazyMergeTarget` (`@state`) so
	 * the swap-in is reactive.
	 */
	override get mergeTarget(): Awaited<GetOverviewBranch['mergeTarget']> {
		return this._lazyMergeTarget;
	}

	override render(): unknown {
		return html`
			<gl-card class="branch-item" focusable .indicator=${this.cardIndicator}>
				<div class="branch-item__container">
					${this.renderBranchItem(this.renderBranchActions())}${this.renderPrItem()}${this.renderIssuesItem()}
				</div>
				${this.renderCollapsedActions()}
			</gl-card>
		`;
	}

	override willUpdate(changed: PropertyValues<this>): void {
		super.willUpdate?.(changed);

		// Drop stale lazy state when Lit `repeat` reuses this card instance for a different branch.
		if (changed.has('branch') && this.branch?.id !== this._lazyMergeTargetFetchedFor) {
			this._lazyMergeTarget = undefined;
			this._lazyMergeTargetPromise = undefined;
			this._lazyMergeTargetFetchedFor = undefined;
			this._lazyMergeTargetLoading = false;
		}

		// First expand for this branch kicks off the fetch; subsequent ticks short-circuit via
		// the `_lazyMergeTargetFetchedFor` dedupe check inside `ensureMergeTargetFetched`.
		if (this.expanded) {
			void this.ensureMergeTargetFetched();
		}
	}

	/**
	 * Render the chip with the subclass's lazy promise + loading flag, replacing the base's
	 * direct read of `this.branch.mergeTarget` (which is `Promise<undefined>` under skip).
	 * Renders nothing until the first expand fires the fetch — matches graph-overview-card.
	 */
	protected override renderMergeTargetStatus(): TemplateResult | NothingType {
		if (this.showUpgrade) {
			return super.renderMergeTargetStatus();
		}

		const promise = this._lazyMergeTargetPromise;
		if (promise == null) return nothing;
		return html`<gl-merge-target-status
			class="branch-item__merge-target"
			.branch=${this.branch}
			.targetPromise=${promise}
			?loading=${this._lazyMergeTargetLoading}
		></gl-merge-target-status>`;
	}

	private async ensureMergeTargetFetched(): Promise<void> {
		const branch = this.branch;
		if (branch == null) return;

		// Already fetched (or in flight) for this branch — nothing to do. The promise is reused
		// across expand cycles; the chip handles loading state internally.
		if (this._lazyMergeTargetFetchedFor === branch.id && this._lazyMergeTargetPromise != null) {
			return;
		}

		// Pro gate — mirror the eager path's server-side `isPro` filter. Without this, non-pro
		// accounts would spin the `aria-busy="true"` loading chip forever because the server
		// fast-bails and the resolved promise never produces a status. `<gl-merge-target-upgrade>`
		// is rendered separately via `showUpgrade` so non-pro still sees the upgrade prompt.
		const subState = this._subscription.subscription.get()?.state;
		if (subState != null && !isSubscriptionTrialOrPaidFromState(subState)) return;

		const branches = this._homeState?.branchesService;
		if (branches == null) return;

		this._lazyMergeTargetFetchedFor = branch.id;
		this._lazyMergeTargetLoading = true;
		const branchId = branch.id;
		const repoPath = this.repo;
		const branchName = branch.name;

		const promise = (async (): Promise<Awaited<GetOverviewBranch['mergeTarget']>> => {
			try {
				const enrichment = await branches.getBranchEnrichment(repoPath, branchName);
				return await enrichment?.mergeTargetStatus;
			} catch {
				return undefined;
			}
		})();
		this._lazyMergeTargetPromise = promise;

		const result = await promise;
		// Bail if a `branch` prop transition reassigned us mid-await — `willUpdate` already
		// cleared the state for the new branch and `_lazyMergeTargetFetchedFor` no longer matches.
		if (this._lazyMergeTargetFetchedFor !== branchId) return;

		this._lazyMergeTarget = result;
		this._lazyMergeTargetLoading = false;
	}

	protected getCollapsedActions(): TemplateResult[] {
		const actions = [];

		if (this.isWorktree) {
			actions.push(
				html`<action-item
					label="Open Worktree"
					alt-label="Open Worktree in New Window"
					icon="browser"
					alt-icon="empty-window"
					href=${this.createWebviewCommandLinkWithBranchRef('gitlens.openWorktree:')}
					alt-href=${this.createWebviewCommandLinkWithBranchRef<OpenWorktreeCommandArgs>(
						'gitlens.openWorktree:',
						{ location: 'newWindow' },
					)}
				></action-item>`,
			);
		} else {
			actions.push(
				html`<action-item
					label="Switch to Branch..."
					icon="gl-switch"
					href=${this.createWebviewCommandLinkWithBranchRef('gitlens.switchToBranch:')}
				></action-item>`,
			);
		}

		actions.push(
			html`<action-item
				label="Open in Commit Graph"
				icon="gl-graph"
				href=${this.createWebviewCommandLinkWithBranchRef<OpenInGraphParams>('gitlens.showInCommitGraph:', {
					type: 'branch',
				})}
			></action-item>`,
			html`<action-item
				label=${this.isWorktree ? 'Open in Worktrees View' : 'Open in Branches View'}
				icon="arrow-right"
				href=${this.createWebviewCommandLinkWithBranchRef('gitlens.openInView.branch:')}
			></action-item>`,
		);

		return actions;
	}

	protected getBranchActions(): TemplateResult[] {
		const actions = [];

		const aiEnabled = this._subscription.orgSettings.get()?.ai && this._aiCtx.state.get().enabled;

		if (this.isWorktree) {
			actions.push(
				html`<action-item
					label="Open Worktree"
					alt-label="Open Worktree in New Window"
					icon="browser"
					alt-icon="empty-window"
					href=${this.createWebviewCommandLinkWithBranchRef('gitlens.openWorktree:')}
					alt-href=${this.createWebviewCommandLinkWithBranchRef<OpenWorktreeCommandArgs>(
						'gitlens.openWorktree:',
						{ location: 'newWindow' },
					)}
				></action-item>`,
			);

			if (aiEnabled) {
				const hasWip =
					this.wip?.workingTreeState != null
						? this.wip.workingTreeState.added +
								this.wip.workingTreeState.changed +
								this.wip.workingTreeState.deleted >
							0
						: false;
				if (hasWip) {
					actions.push(
						html`<action-item
							label="Explain Working Changes"
							icon="sparkle"
							href=${this.createWebviewCommandLinkWithBranchRef('gitlens.ai.explainWip:')}
						></action-item>`,
					);
				} else {
					actions.push(
						html`<action-item
							label="Explain Branch Changes"
							icon="sparkle"
							href=${this.createWebviewCommandLinkWithBranchRef('gitlens.ai.explainBranch:')}
						></action-item>`,
					);
				}
			}
		} else {
			actions.push(
				html`<action-item
					label="Switch to Branch..."
					icon="gl-switch"
					href=${this.createWebviewCommandLinkWithBranchRef('gitlens.switchToBranch:')}
				></action-item>`,
			);

			if (aiEnabled) {
				actions.push(
					html`<action-item
						label="Explain Branch Changes"
						icon="sparkle"
						href=${this.createWebviewCommandLinkWithBranchRef('gitlens.ai.explainBranch:')}
					></action-item>`,
				);
			}
		}

		// branch actions
		actions.push(
			html`<action-item
				label="Fetch"
				icon="repo-fetch"
				href=${this.createWebviewCommandLinkWithBranchRef('gitlens.fetch:')}
			></action-item>`,
		);
		actions.push(
			html` <action-item
				label="Visualize Branch History"
				icon="graph-scatter"
				href=${this._webview.createCommandLink<OpenInTimelineParams>('gitlens.visualizeHistory.branch:', {
					type: 'branch',
					repoPath: this.repo,
					branchId: this.branch.id,
				})}
			></action-item>`,
		);
		actions.push(
			html`<action-item
				label="Open in Commit Graph"
				icon="gl-graph"
				href=${this.createWebviewCommandLinkWithBranchRef<OpenInGraphParams>('gitlens.showInCommitGraph:', {
					type: 'branch',
				})}
			></action-item>`,
			html`<action-item
				label=${this.isWorktree ? 'Open in Worktrees View' : 'Open in Branches View'}
				icon="arrow-right"
				href=${this.createWebviewCommandLinkWithBranchRef('gitlens.openInView.branch:')}
			></action-item>`,
		);

		return actions;
	}

	protected getPrActions(): TemplateResult[] {
		return [
			html`<action-item
				label="Open Pull Request Changes"
				icon="request-changes"
				href=${this.createWebviewCommandLinkWithBranchRef('gitlens.openPullRequestChanges:')}
			></action-item>`,
			html`<action-item
				label="Compare Pull Request"
				icon="git-compare"
				href=${this.createWebviewCommandLinkWithBranchRef('gitlens.openPullRequestComparison:')}
			></action-item>`,
			html`<action-item
				label="Open Pull Request Details"
				icon="eye"
				href=${this.createWebviewCommandLinkWithBranchRef('gitlens.openPullRequestDetails:')}
			></action-item>`,
		];
	}

	renderBranchIndicator(): TemplateResult | undefined {
		return undefined;
	}
}

// Re-export GlWorkUnit from shared (component registration is side-effect of the import)
export { GlWorkUnit } from '../../../shared/components/card/work-item.js';
import '../../../shared/components/card/work-item.js';

function getLaunchpadItemGroup(
	pr: Awaited<GetOverviewBranch['pr']>,
	launchpadItem: Awaited<NonNullable<Awaited<GetOverviewBranch['pr']>>['launchpad']>,
) {
	if (launchpadItem == null || pr?.state !== 'opened') return undefined;
	if (pr.draft && launchpadItem.category === 'unassigned-reviewers') return undefined;

	const group = launchpadCategoryToGroupMap.get(launchpadItem.category);
	if (group == null || group === 'other' || group === 'draft' || group === 'current-branch') {
		return undefined;
	}

	return group;
}

function getLaunchpadItemGrouping(group: ReturnType<typeof getLaunchpadItemGroup>) {
	switch (group) {
		case 'mergeable':
			return 'mergeable';
		case 'blocked':
			return 'blocked';
		case 'follow-up':
		case 'needs-review':
			return 'attention';
	}

	return undefined;
}
