1import type { ArchivedSnapshotEntry } from "@t3tools/client-runtime/state/threads";
2import {
3 scopeProject,
4 scopeThreadShell,
5 type EnvironmentProject,
6 type EnvironmentThreadShell,
7} from "@t3tools/client-runtime/state/shell";
8import type { EnvironmentId } from "@t3tools/contracts";
9import * as Arr from "effect/Array";
10import * as Order from "effect/Order";
11
12import { scopedProjectKey } from "../../lib/scopedEntities";
13
14export type ArchivedThreadSortOrder = "newest" | "oldest";
15
16export interface ArchivedThreadGroup {
17 readonly key: string;
18 readonly project: EnvironmentProject;
19 readonly threads: ReadonlyArray<EnvironmentThreadShell>;
20}
21
22function archiveTimestamp(thread: EnvironmentThreadShell): number {
23 const timestamp = Date.parse(thread.archivedAt ?? thread.updatedAt);
24 return Number.isNaN(timestamp) ? 0 : timestamp;
25}
26
27function matchesQuery(value: string | null, query: string): boolean {
28 return value?.toLocaleLowerCase().includes(query) ?? false;
29}
30
31export function buildArchivedThreadGroups(input: {
32 readonly snapshots: ReadonlyArray<ArchivedSnapshotEntry>;
33 readonly environmentLabels: Readonly<Record<string, string>>;
34 readonly environmentId: EnvironmentId | null;
35 readonly searchQuery: string;
36 readonly sortOrder: ArchivedThreadSortOrder;
37}): ReadonlyArray<ArchivedThreadGroup> {
38 const query = input.searchQuery.trim().toLocaleLowerCase();
39 const groups: ArchivedThreadGroup[] = [];
40
41 for (const entry of input.snapshots) {
42 if (input.environmentId !== null && input.environmentId !== entry.environmentId) {
43 continue;
44 }
45
46 const environmentLabel = input.environmentLabels[entry.environmentId] ?? null;
47 const threadsByProjectId = new Map<string, EnvironmentThreadShell[]>();
48 for (const thread of entry.snapshot.threads) {
49 if (thread.archivedAt === null) {
50 continue;
51 }
52 const threads = threadsByProjectId.get(thread.projectId) ?? [];
53 threads.push(scopeThreadShell(entry.environmentId, thread));
54 threadsByProjectId.set(thread.projectId, threads);
55 }
56
57 for (const rawProject of entry.snapshot.projects) {
58 const project = scopeProject(entry.environmentId, rawProject);
59 const projectThreads = threadsByProjectId.get(project.id) ?? [];
60 const groupMatches =
61 query.length === 0 ||
62 matchesQuery(project.title, query) ||
63 matchesQuery(project.workspaceRoot, query) ||
64 matchesQuery(environmentLabel, query);
65 const matchingThreads = groupMatches
66 ? projectThreads
67 : projectThreads.filter(
68 (thread) => matchesQuery(thread.title, query) || matchesQuery(thread.branch, query),
69 );
70
71 if (matchingThreads.length === 0) {
72 continue;
73 }
74
75 const timestampOrder = input.sortOrder === "newest" ? Order.flip(Order.Number) : Order.Number;
76 groups.push({
77 key: scopedProjectKey(project.environmentId, project.id),
78 project,
79 threads: Arr.sort(
80 matchingThreads,
81 Order.mapInput(
82 Order.Struct({ timestamp: timestampOrder, title: Order.String, id: Order.String }),
83 (thread: EnvironmentThreadShell) => ({
84 timestamp: archiveTimestamp(thread),
85 title: thread.title,
86 id: thread.id,
87 }),
88 ),
89 ),
90 });
91 }
92 }
93
94 const timestampOrder = input.sortOrder === "newest" ? Order.flip(Order.Number) : Order.Number;
95 return Arr.sort(
96 groups,
97 Order.mapInput(
98 Order.Struct({ timestamp: timestampOrder, title: Order.String, key: Order.String }),
99 (group: ArchivedThreadGroup) => ({
100 timestamp: group.threads[0] ? archiveTimestamp(group.threads[0]) : 0,
101 title: group.project.title,
102 key: group.key,
103 }),
104 ),
105 );
106}