/**
 * dashboard-client/src/components/RepoContextStack.tsx — Stacked per-repo context bar (S40).
 *
 * Horizontal stacked bar: one colored segment per active repo, aggregating
 * that repo's active sessions' token counts. Renders a combined total line
 * and a legend mapping each repo (basename of repoRoot) to its segment color
 * + token count + % of total.
 *
 * Stable per-repo colors: the repoRoot basename is hashed to a deterministic
 * index into a fixed palette array, so the same repo always gets the same
 * color across renders/tabs (mirrors SessionsMemoryChart's stable-color
 * contract from S39).
 *
 * PREVENT-PI-004: this is a pure presentational component — no fetch, no
 * network. Data is passed in by the parent (OverviewTab) via
 * useApi(fetchSessions) + useSSE() for real-time updates.
 */

import type React from "react";
import { useRef } from "react";
import type { ActiveSession, SessionsResponse } from "@contracts";
import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card";

export interface RepoContextStackProps {
	/** Active sessions from /api/sessions (may be null while loading). */
	sessions: SessionsResponse | null;
	/** Whether the initial fetch is in flight. */
	loading: boolean;
	/** Fetch error, if any. */
	error: Error | null;
}

// ─── Stable color palette ─────────────────────────────────────────────────────
// Fixed set of hex colors used for stable per-repo coloring. The hash function
// maps a repoRoot basename to a deterministic palette index so the same repo
// always renders the same color.
const REPO_PALETTE: ReadonlyArray<string> = [
	"#58a6ff", // blue
	"#3fb950", // green
	"#d29922", // yellow
	"#f85149", // red
	"#bc8cff", // purple
	"#ff7b72", // salmon
	"#79c0ff", // light blue
	"#56d4dd", // cyan
	"#e3b341", // gold
	"#a5d6ff", // pale blue
	"#7ee787", // mint
	"#ffa657", // orange
];

/**
 * Simple deterministic string hash (djb2 variant). Returns a non-negative
 * 32-bit integer.
 */
function hashString(str: string): number {
	let hash = 5381;
	for (let i = 0; i < str.length; i++) {
		hash = ((hash << 5) + hash + str.charCodeAt(i)) >>> 0;
	}
	return hash;
}

/**
 * Pick a stable color for a repoRoot basename. Falls back to "(unknown)" if
 * the basename is empty.
 */
function colorForRepo(basename: string): string {
	if (!basename) return REPO_PALETTE[0];
	return REPO_PALETTE[hashString(basename) % REPO_PALETTE.length];
}

/**
 * Extract the basename from a repoRoot path. Uses `"/"` as the universal
 * separator on all OSes (repoRoot is an absolute path from the dashboard
 * server; on Windows pi actually uses forward-slash normalized paths anyway).
 */
function repoBasename(repoRoot: string | null): string {
	if (!repoRoot) return "(unknown)";
	const trimmed = repoRoot.replace(/[\\/]+$/, "");
	if (!trimmed) return "(unknown)";
	const parts = trimmed.split(/[\\/]/);
	const last = parts[parts.length - 1];
	return last || trimmed;
}

// ─── Aggregation ─────────────────────────────────────────────────────────────

interface RepoAggregate {
	repoRoot: string | null;
	basename: string;
	tokens: number;
	color: string;
}

function aggregateRepos(sessions: ReadonlyArray<ActiveSession>): {
	repos: RepoAggregate[];
	totalTokens: number;
	maxWindow: number;
	sessionCount: number;
} {
	const map = new Map<string, RepoAggregate>();
	for (const s of sessions) {
		const key = s.repoRoot ?? "__null__";
		const tokens = s.tokens ?? 0;
		const existing = map.get(key);
		if (existing) {
			existing.tokens += tokens;
		} else {
			const basename = repoBasename(s.repoRoot);
			map.set(key, {
				repoRoot: s.repoRoot,
				basename,
				tokens,
				color: colorForRepo(basename),
			});
		}
	}
	const repos = [...map.values()];
	const totalTokens = repos.reduce((sum, r) => sum + r.tokens, 0);
	// Combined max window: the largest ctxWindow among the active sessions.
	// This mirrors SessionsTab's SummaryTiles combined-% logic.
	const maxWindow = sessions.reduce(
		(max, s) => Math.max(max, s.ctxWindow),
		0,
	);
	return {
		repos,
		totalTokens,
		maxWindow,
		sessionCount: sessions.length,
	};
}

function fmtTokens(n: number): string {
	if (n >= 100_000) return `${(n / 1000).toFixed(0)}k`;
	return n.toLocaleString();
}

function StackCard({ children }: { children: React.ReactNode }): React.ReactElement {
	return (
		<Card className="electric-hover">
			<CardHeader>
				<CardTitle>Context per Repo (live)</CardTitle>
			</CardHeader>
			<CardContent>{children}</CardContent>
		</Card>
	);
}

export function RepoContextStack({
	sessions,
	loading,
	error,
}: RepoContextStackProps): React.ReactElement | null {
	// Hysteresis: retain the last non-empty aggregates so a transiently-empty
	// /api/sessions poll (prune-vs-heartbeat race) doesn't unmount/remount the
	// chart (the visible "blinking"). We only fall back to "No active sessions."
	// when there has NEVER been data (initial mount) or after a sustained empty.
	const lastNonEmpty = useRef<{
		repos: RepoAggregate[];
		totalTokens: number;
		maxWindow: number;
		sessionCount: number;
	} | null>(null);

	if (error) {
		return (
			<StackCard>
				<span className="text-sm text-muted-foreground">
					Error loading sessions: {error.message}
				</span>
			</StackCard>
		);
	}

	if (loading && !sessions) {
		return (
			<StackCard>
				<span className="text-sm text-muted-foreground">Loading sessions…</span>
			</StackCard>
		);
	}

	const activeSessions = sessions?.sessions ?? [];
	const { repos, totalTokens, maxWindow, sessionCount } =
		aggregateRepos(activeSessions);

	if (repos.length > 0) {
		lastNonEmpty.current = { repos, totalTokens, maxWindow, sessionCount };
	}

	// Use last-known-good data when the current poll is transiently empty.
	const displayed =
		repos.length > 0
			? { repos, totalTokens, maxWindow, sessionCount }
			: lastNonEmpty.current;

	if (!displayed) {
		return (
			<StackCard>
				<span className="text-sm text-muted-foreground">No active sessions.</span>
			</StackCard>
		);
	}

	const combinedPct =
		displayed.maxWindow > 0
			? Math.round((displayed.totalTokens / displayed.maxWindow) * 100)
			: 0;

	return (
		<StackCard>
			<div
				className="repo-stack-bar"
				role="group"
				aria-label="Per-repo context usage"
			>
				{displayed.repos.map((r) => {
					const pctOfTotal =
						displayed.totalTokens > 0
							? (r.tokens / displayed.totalTokens) * 100
							: 0;
					const widthPct = displayed.totalTokens > 0 ? pctOfTotal : 0;
					const tooltip =
						`${r.basename}: ${fmtTokens(r.tokens)} tokens ` +
						`(${pctOfTotal.toFixed(1)}% of total)`;
					return (
						<div
							key={r.repoRoot ?? "__null__"}
							className="repo-stack-segment"
							style={{
								width: `${Math.max(widthPct, 0.5)}%`,
								backgroundColor: r.color,
							}}
							title={tooltip}
							aria-label={tooltip}
						/>
					);
				})}
			</div>
			<p className="repo-stack-total">
				{fmtTokens(displayed.totalTokens)} tokens across {displayed.repos.length}{" "}
				{displayed.repos.length === 1 ? "repo" : "repos"} /{" "}
				{displayed.sessionCount}{" "}
				{displayed.sessionCount === 1 ? "session" : "sessions"}{" "}
				<span className="pct">— {combinedPct}% of combined max window</span>
			</p>
			<div className="repo-stack-legend">
				{displayed.repos.map((r) => {
					const pctOfTotal =
						displayed.totalTokens > 0 ? (r.tokens / displayed.totalTokens) * 100 : 0;
					return (
						<div
							key={r.repoRoot ?? "__null__"}
							className="repo-stack-legend-item"
						>
							<span
								className="repo-stack-legend-swatch"
								style={{ backgroundColor: r.color }}
							/>
							<span className="repo-stack-legend-name">{r.basename}</span>
							<span>
								{fmtTokens(r.tokens)} ({pctOfTotal.toFixed(0)}%)
							</span>
						</div>
					);
				})}
			</div>
		</StackCard>
	);
}
