import { getTasksBasePath, getTeamsBasePath } from '@main/utils/pathDecoder';
import { createLogger } from '@shared/utils/logger';
import * as fs from 'fs';
import * as path from 'path';

import { withFileLockSync } from './fileLock';
import { TeamTaskReader } from './TeamTaskReader';

import type {
  PersistedTeamLaunchMemberState,
  PersistedTeamLaunchSnapshot,
  TaskReviewInterval,
  TeamTask,
} from '@shared/types';

interface ActivityIntervalResult {
  changedTasks: number;
  failed?: boolean;
}

interface TaskDirectorySignature {
  key: string;
}

interface TaskFileSignature {
  size: number;
  mtimeMs: number;
  ctimeMs: number;
  dev: number;
  ino: number;
}

interface CachedActivityTaskFile {
  signature: TaskFileSignature;
  task: MutableTeamTask | null;
}

interface ResumeMembersCacheEntry {
  memberKey: string;
  signatureKey: string;
}

type MemberActivityNoopOperation = 'pause-member' | 'resume-member';

type MutableTeamTask = TeamTask & {
  reviewIntervals?: TaskReviewInterval[];
};

const CRASH_REPAIR_GRACE_MS = 5_000;
const TASK_FILE_CACHE_MAX_ENTRIES = 8_192;
const logger = createLogger('Service:TeamTaskActivityIntervalService');

function normalizeMemberName(value: string | null | undefined): string {
  return typeof value === 'string' && value.trim() ? value.trim().toLowerCase() : '';
}

function parseIsoMs(value: string | null | undefined): number {
  if (!value) return 0;
  const parsed = Date.parse(value);
  return Number.isFinite(parsed) ? parsed : 0;
}

function toIso(ms: number): string {
  return new Date(ms).toISOString();
}

function isClosedInterval(interval: { completedAt?: unknown } | null | undefined): boolean {
  return typeof interval?.completedAt === 'string' && parseIsoMs(interval.completedAt) > 0;
}

function hasValidStartedAt(interval: { startedAt?: unknown } | null | undefined): boolean {
  return typeof interval?.startedAt === 'string' && parseIsoMs(interval.startedAt) > 0;
}

function ensureCloseIso(startedAt: string, at: string): string {
  const startedAtMs = parseIsoMs(startedAt);
  const atMs = parseIsoMs(at);
  if (startedAtMs <= 0) return at;
  if (atMs <= startedAtMs) return toIso(startedAtMs);
  return toIso(atMs);
}

function resumeStartIso(activeStartedAt: string | null | undefined, at: string): string {
  const activeStartedAtMs = parseIsoMs(activeStartedAt ?? undefined);
  const atMs = parseIsoMs(at);
  if (activeStartedAtMs > 0 && activeStartedAtMs > atMs) {
    return toIso(activeStartedAtMs);
  }
  return atMs > 0 ? toIso(atMs) : toIso(Date.now());
}

function getStartedAtString(interval: { startedAt?: unknown } | null | undefined): string {
  return typeof interval?.startedAt === 'string' ? interval.startedAt : '';
}

function hasUsableCompletedAt(interval: { completedAt?: unknown } | null | undefined): boolean {
  return interval?.completedAt === undefined || isClosedInterval(interval);
}

function pauseCloseIso(
  interval: { startedAt?: unknown; completedAt?: unknown } | null | undefined,
  at: string
): string {
  const startedAt = getStartedAtString(interval);
  const closeAt = interval?.completedAt === undefined ? at : startedAt || at;
  return ensureCloseIso(startedAt, closeAt);
}

function crashRepairCloseIso(startedAt: string, member?: PersistedTeamLaunchMemberState): string {
  const startedAtMs = parseIsoMs(startedAt);
  const safeStartedAtMs = startedAtMs > 0 ? startedAtMs : Date.now();
  const evidenceMs = Math.max(
    parseIsoMs(member?.lastHeartbeatAt),
    parseIsoMs(member?.runtimeLastSeenAt),
    parseIsoMs(member?.lastRuntimeAliveAt)
  );
  const closeMs =
    evidenceMs > 0
      ? Math.max(safeStartedAtMs, evidenceMs + CRASH_REPAIR_GRACE_MS)
      : safeStartedAtMs + CRASH_REPAIR_GRACE_MS;
  const boundedCloseMs = Math.max(safeStartedAtMs, Math.min(Date.now(), closeMs));
  return toIso(boundedCloseMs);
}

function crashRepairIntervalCloseIso(
  interval: { startedAt?: unknown; completedAt?: unknown } | null | undefined,
  member?: PersistedTeamLaunchMemberState
): string {
  const startedAt = getStartedAtString(interval);
  if (interval?.completedAt === undefined) {
    return crashRepairCloseIso(startedAt, member);
  }
  return ensureCloseIso(startedAt, startedAt || crashRepairCloseIso(startedAt, member));
}

function hasOpenWorkInterval(task: MutableTeamTask): boolean {
  return (
    Array.isArray(task.workIntervals) &&
    task.workIntervals.some(
      (interval) => hasValidStartedAt(interval) && interval.completedAt === undefined
    )
  );
}

function hasOpenReviewInterval(task: MutableTeamTask, reviewer: string): boolean {
  const reviewerKey = normalizeMemberName(reviewer);
  return (
    Array.isArray(task.reviewIntervals) &&
    task.reviewIntervals.some(
      (interval) =>
        hasValidStartedAt(interval) &&
        interval.completedAt === undefined &&
        normalizeMemberName(interval.reviewer) === reviewerKey
    )
  );
}

function closeOpenWorkIntervals(task: MutableTeamTask, at: string, owner?: string): boolean {
  if (!Array.isArray(task.workIntervals)) return false;
  if (owner && normalizeMemberName(task.owner) !== normalizeMemberName(owner)) return false;

  let changed = false;
  task.workIntervals = task.workIntervals.map((interval) => {
    if (isClosedInterval(interval)) return interval;
    changed = true;
    return { ...interval, completedAt: pauseCloseIso(interval, at) };
  });
  return changed;
}

function closeOpenReviewIntervals(task: MutableTeamTask, at: string, reviewer?: string): boolean {
  if (!Array.isArray(task.reviewIntervals)) return false;
  const reviewerKey = normalizeMemberName(reviewer);

  let changed = false;
  task.reviewIntervals = task.reviewIntervals.map((interval) => {
    if (isClosedInterval(interval)) return interval;
    if (reviewerKey && normalizeMemberName(interval.reviewer) !== reviewerKey) return interval;
    changed = true;
    return { ...interval, completedAt: pauseCloseIso(interval, at) };
  });
  return changed;
}

function getActiveWorkStartedAt(task: MutableTeamTask): string | null {
  const events = Array.isArray(task.historyEvents) ? task.historyEvents : [];
  for (let index = events.length - 1; index >= 0; index -= 1) {
    const event = events[index];
    if (event.type === 'status_changed') {
      if (event.to === 'in_progress') {
        return parseIsoMs(event.timestamp) > 0 ? event.timestamp : null;
      }
      return null;
    }
    if (event.type === 'task_created') {
      return event.status === 'in_progress' && parseIsoMs(event.timestamp) > 0
        ? event.timestamp
        : null;
    }
  }
  return null;
}

function getActiveReviewStart(
  task: MutableTeamTask
): { reviewer: string; startedAt: string } | null {
  const events = Array.isArray(task.historyEvents) ? task.historyEvents : [];
  for (let index = events.length - 1; index >= 0; index -= 1) {
    const event = events[index];
    if (event.type === 'review_started') {
      const reviewer =
        typeof event.actor === 'string' && event.actor.trim() ? event.actor.trim() : '';
      return reviewer && parseIsoMs(event.timestamp) > 0
        ? { reviewer, startedAt: event.timestamp }
        : null;
    }
    if (
      event.type === 'review_approved' ||
      event.type === 'review_changes_requested' ||
      (event.type === 'status_changed' &&
        (event.to === 'in_progress' || event.to === 'pending' || event.to === 'deleted')) ||
      event.type === 'task_created'
    ) {
      return null;
    }
  }
  return null;
}

function hasWorkIntervalForStart(task: MutableTeamTask, startedAt: string): boolean {
  const startedAtMs = parseIsoMs(startedAt);
  return (
    startedAtMs > 0 &&
    Array.isArray(task.workIntervals) &&
    task.workIntervals.some((interval) => parseIsoMs(interval.startedAt) === startedAtMs)
  );
}

function hasPersistedWorkIntervalAtOrAfter(task: MutableTeamTask, startedAt: string): boolean {
  const startedAtMs = parseIsoMs(startedAt);
  return (
    startedAtMs > 0 &&
    Array.isArray(task.workIntervals) &&
    task.workIntervals.some(
      (interval) =>
        hasValidStartedAt(interval) &&
        hasUsableCompletedAt(interval) &&
        parseIsoMs(interval.startedAt) >= startedAtMs
    )
  );
}

function hasReviewIntervalForStart(
  task: MutableTeamTask,
  reviewer: string,
  startedAt: string
): boolean {
  const reviewerKey = normalizeMemberName(reviewer);
  const startedAtMs = parseIsoMs(startedAt);
  return (
    reviewerKey.length > 0 &&
    startedAtMs > 0 &&
    Array.isArray(task.reviewIntervals) &&
    task.reviewIntervals.some(
      (interval) =>
        normalizeMemberName(interval.reviewer) === reviewerKey &&
        parseIsoMs(interval.startedAt) === startedAtMs
    )
  );
}

function hasPersistedReviewIntervalAtOrAfter(task: MutableTeamTask, startedAt: string): boolean {
  const startedAtMs = parseIsoMs(startedAt);
  return (
    startedAtMs > 0 &&
    Array.isArray(task.reviewIntervals) &&
    task.reviewIntervals.some(
      (interval) =>
        normalizeMemberName(interval?.reviewer).length > 0 &&
        hasValidStartedAt(interval) &&
        hasUsableCompletedAt(interval) &&
        parseIsoMs(interval.startedAt) >= startedAtMs
    )
  );
}

function materializePausedWorkInterval(task: MutableTeamTask, at: string, owner?: string): boolean {
  if (task.status !== 'in_progress') return false;
  if (owner && normalizeMemberName(task.owner) !== normalizeMemberName(owner)) return false;

  const startedAt = getActiveWorkStartedAt(task);
  if (
    !startedAt ||
    hasPersistedWorkIntervalAtOrAfter(task, startedAt) ||
    hasWorkIntervalForStart(task, startedAt)
  ) {
    return false;
  }
  task.workIntervals = [
    ...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
    { startedAt, completedAt: ensureCloseIso(startedAt, at) },
  ];
  return true;
}

function materializePausedReviewInterval(
  task: MutableTeamTask,
  at: string,
  reviewer?: string
): boolean {
  if (task.status !== 'completed') return false;
  const activeReview = getActiveReviewStart(task);
  if (!activeReview) return false;
  if (reviewer && normalizeMemberName(activeReview.reviewer) !== normalizeMemberName(reviewer)) {
    return false;
  }
  if (
    hasPersistedReviewIntervalAtOrAfter(task, activeReview.startedAt) ||
    hasReviewIntervalForStart(task, activeReview.reviewer, activeReview.startedAt)
  ) {
    return false;
  }
  task.reviewIntervals = [
    ...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
    {
      reviewer: activeReview.reviewer,
      startedAt: activeReview.startedAt,
      completedAt: ensureCloseIso(activeReview.startedAt, at),
    },
  ];
  return true;
}

function writeTaskFile(filePath: string, task: MutableTeamTask): void {
  const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
  fs.writeFileSync(tempPath, JSON.stringify(task, null, 2));
  fs.renameSync(tempPath, filePath);
}

function buildTaskFileSignature(stat: fs.Stats): TaskFileSignature {
  return {
    size: stat.size,
    mtimeMs: stat.mtimeMs,
    ctimeMs: stat.ctimeMs,
    dev: stat.dev,
    ino: stat.ino,
  };
}

function taskFileSignaturesEqual(a: TaskFileSignature, b: TaskFileSignature): boolean {
  return (
    a.size === b.size &&
    a.mtimeMs === b.mtimeMs &&
    a.ctimeMs === b.ctimeMs &&
    a.dev === b.dev &&
    a.ino === b.ino
  );
}

export class TeamTaskActivityIntervalService {
  private readonly resumeMembersCache = new Map<string, ResumeMembersCacheEntry>();
  private readonly memberActivityNoopCache = new Map<string, string>();
  private readonly taskFileCache = new Map<string, CachedActivityTaskFile>();

  private getBoardStateLockPath(teamName: string): string {
    return `${path.join(getTeamsBasePath(), teamName, 'board-state')}.lock`;
  }

  private getMemberActivityNoopCacheKey(
    teamName: string,
    operation: MemberActivityNoopOperation,
    memberKey: string
  ): string {
    return `${teamName}\u0000${operation}\u0000${memberKey}`;
  }

  private clearMemberActivityNoopCacheForTeam(teamName: string): void {
    const prefix = `${teamName}\u0000`;
    for (const key of this.memberActivityNoopCache.keys()) {
      if (key.startsWith(prefix)) {
        this.memberActivityNoopCache.delete(key);
      }
    }
  }

  private clearActivityNoopCachesForTeam(teamName: string): void {
    this.clearMemberActivityNoopCacheForTeam(teamName);
    this.resumeMembersCache.delete(teamName);
  }

  private getCachedTaskFile(
    filePath: string,
    signature: TaskFileSignature
  ): MutableTeamTask | null | undefined {
    const cached = this.taskFileCache.get(filePath);
    if (!cached) return undefined;
    if (!taskFileSignaturesEqual(cached.signature, signature)) {
      this.taskFileCache.delete(filePath);
      return undefined;
    }
    return cached.task ? structuredClone(cached.task) : null;
  }

  private setCachedTaskFile(
    filePath: string,
    signature: TaskFileSignature,
    task: MutableTeamTask | null
  ): void {
    if (
      !this.taskFileCache.has(filePath) &&
      this.taskFileCache.size >= TASK_FILE_CACHE_MAX_ENTRIES
    ) {
      const oldestKey = this.taskFileCache.keys().next().value;
      if (oldestKey) {
        this.taskFileCache.delete(oldestKey);
      }
    }
    this.taskFileCache.set(filePath, {
      signature,
      task: task ? structuredClone(task) : null,
    });
  }

  private readTaskFile(filePath: string): MutableTeamTask | null {
    let signature: TaskFileSignature;
    try {
      const stat = fs.statSync(filePath);
      if (!stat.isFile()) {
        this.taskFileCache.delete(filePath);
        return null;
      }
      signature = buildTaskFileSignature(stat);
      const cached = this.getCachedTaskFile(filePath, signature);
      if (cached !== undefined) {
        return cached;
      }
    } catch {
      this.taskFileCache.delete(filePath);
      return null;
    }

    try {
      const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8')) as unknown;
      const task = parsed && typeof parsed === 'object' ? (parsed as MutableTeamTask) : null;
      this.setCachedTaskFile(filePath, signature, task);
      return task;
    } catch {
      this.setCachedTaskFile(filePath, signature, null);
      return null;
    }
  }

  private cacheWrittenTaskFile(filePath: string, task: MutableTeamTask): void {
    try {
      const stat = fs.statSync(filePath);
      if (!stat.isFile()) {
        this.taskFileCache.delete(filePath);
        return;
      }
      this.setCachedTaskFile(filePath, buildTaskFileSignature(stat), task);
    } catch {
      this.taskFileCache.delete(filePath);
    }
  }

  private mutateTeamTasksWithLock(
    teamName: string,
    run: () => ActivityIntervalResult
  ): ActivityIntervalResult {
    const lockScope = path.join(getTeamsBasePath(), teamName, 'board-state');
    try {
      return withFileLockSync(lockScope, run);
    } catch (error) {
      logger.warn(
        `[${teamName}] Failed to update task activity intervals: ${
          error instanceof Error ? error.message : String(error)
        }`
      );
      return { changedTasks: 0, failed: true };
    }
  }

  private mutateTeamTasks(
    teamName: string,
    mutate: (task: MutableTeamTask) => boolean
  ): ActivityIntervalResult {
    const result = this.mutateTeamTasksWithLock(teamName, () =>
      this.mutateTeamTasksUnlocked(teamName, mutate)
    );
    if (result.changedTasks > 0 || result.failed) {
      this.clearActivityNoopCachesForTeam(teamName);
    }
    return result;
  }

  private mutateMemberTasksWithNoopCache(
    teamName: string,
    operation: MemberActivityNoopOperation,
    memberKey: string,
    mutate: (task: MutableTeamTask) => boolean
  ): ActivityIntervalResult {
    const cacheKey = this.getMemberActivityNoopCacheKey(teamName, operation, memberKey);
    const cachedSignatureKey = this.memberActivityNoopCache.get(cacheKey);
    if (cachedSignatureKey) {
      const beforeLockSignature = this.readTaskDirectorySignature(teamName);
      if (
        beforeLockSignature &&
        beforeLockSignature.key === cachedSignatureKey &&
        !fs.existsSync(this.getBoardStateLockPath(teamName))
      ) {
        return { changedTasks: 0 };
      }
    }

    const result = this.mutateTeamTasksWithLock(teamName, () => {
      const beforeSignature = this.readTaskDirectorySignature(teamName);
      if (beforeSignature && this.memberActivityNoopCache.get(cacheKey) === beforeSignature.key) {
        return { changedTasks: 0 };
      }

      const mutationResult = this.mutateTeamTasksUnlocked(teamName, mutate);
      if (mutationResult.changedTasks > 0) {
        this.clearActivityNoopCachesForTeam(teamName);
        return mutationResult;
      }

      const nextSignature = beforeSignature ?? this.readTaskDirectorySignature(teamName);
      if (nextSignature) {
        this.memberActivityNoopCache.set(cacheKey, nextSignature.key);
      } else {
        this.memberActivityNoopCache.delete(cacheKey);
      }
      return mutationResult;
    });

    if (result.changedTasks > 0 || result.failed) {
      this.clearActivityNoopCachesForTeam(teamName);
    }
    return result;
  }

  private mutateTeamTasksUnlocked(
    teamName: string,
    mutate: (task: MutableTeamTask) => boolean
  ): ActivityIntervalResult {
    const tasksDir = path.join(getTasksBasePath(), teamName);
    let entries: string[];
    try {
      entries = fs.readdirSync(tasksDir);
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
        return { changedTasks: 0 };
      }
      throw error;
    }

    let changedTasks = 0;
    for (const fileName of entries) {
      if (!fileName.endsWith('.json') || fileName.startsWith('.')) continue;
      const filePath = path.join(tasksDir, fileName);
      const task = this.readTaskFile(filePath);
      if (!task) continue;
      if (!mutate(task)) continue;
      writeTaskFile(filePath, task);
      this.cacheWrittenTaskFile(filePath, task);
      changedTasks += 1;
    }

    if (changedTasks > 0) {
      TeamTaskReader.invalidateAllTasksCache();
    }
    return { changedTasks };
  }

  private readTaskDirectorySignature(teamName: string): TaskDirectorySignature | null {
    const tasksDir = path.join(getTasksBasePath(), teamName);
    let entries: string[];
    try {
      entries = fs
        .readdirSync(tasksDir)
        .filter((fileName) => fileName.endsWith('.json') && !fileName.startsWith('.'))
        .sort();
    } catch (error) {
      if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
        return { key: 'missing' };
      }
      return null;
    }

    const parts: string[] = [];
    for (const fileName of entries) {
      try {
        const stat = fs.statSync(path.join(tasksDir, fileName));
        if (!stat.isFile()) continue;
        parts.push([fileName, stat.size, stat.mtimeMs, stat.ctimeMs].join('\0'));
      } catch {
        return null;
      }
    }
    return { key: parts.join('\0\0') };
  }

  private makeMemberSetKey(memberKeys: ReadonlySet<string>): string {
    return [...memberKeys].sort().join('\0');
  }

  pauseActiveIntervalsForTeam(
    teamName: string,
    at = new Date().toISOString()
  ): ActivityIntervalResult {
    return this.mutateTeamTasks(teamName, (task) => {
      const changedWork = closeOpenWorkIntervals(task, at);
      const changedReview = closeOpenReviewIntervals(task, at);
      const materializedWork = materializePausedWorkInterval(task, at);
      const materializedReview = materializePausedReviewInterval(task, at);
      return changedWork || changedReview || materializedWork || materializedReview;
    });
  }

  pauseActiveIntervalsForMember(
    teamName: string,
    memberName: string,
    at = new Date().toISOString()
  ): ActivityIntervalResult {
    const memberKey = normalizeMemberName(memberName);
    const mutate = (task: MutableTeamTask): boolean => {
      const changedWork = closeOpenWorkIntervals(task, at, memberName);
      const changedReview = closeOpenReviewIntervals(task, at, memberName);
      const materializedWork = materializePausedWorkInterval(task, at, memberName);
      const materializedReview = materializePausedReviewInterval(task, at, memberName);
      return changedWork || changedReview || materializedWork || materializedReview;
    };

    if (!memberKey) {
      return this.mutateTeamTasks(teamName, mutate);
    }
    return this.mutateMemberTasksWithNoopCache(teamName, 'pause-member', memberKey, mutate);
  }

  resumeActiveIntervalsForMember(
    teamName: string,
    memberName: string,
    at = new Date().toISOString()
  ): ActivityIntervalResult {
    const memberKey = normalizeMemberName(memberName);
    if (!memberKey) return { changedTasks: 0 };

    return this.mutateMemberTasksWithNoopCache(teamName, 'resume-member', memberKey, (task) => {
      let changed = false;

      if (
        task.status === 'in_progress' &&
        normalizeMemberName(task.owner) === memberKey &&
        !hasOpenWorkInterval(task)
      ) {
        const activeStartedAt = getActiveWorkStartedAt(task);
        task.workIntervals = [
          ...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
          { startedAt: resumeStartIso(activeStartedAt, at) },
        ];
        changed = true;
      }

      const activeReview = getActiveReviewStart(task);
      if (
        task.status === 'completed' &&
        activeReview &&
        normalizeMemberName(activeReview.reviewer) === memberKey &&
        !hasOpenReviewInterval(task, activeReview.reviewer)
      ) {
        task.reviewIntervals = [
          ...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
          {
            reviewer: activeReview.reviewer,
            startedAt: resumeStartIso(activeReview.startedAt, at),
          },
        ];
        changed = true;
      }

      return changed;
    });
  }

  /**
   * Batched equivalent of resumeActiveIntervalsForMember for several members in a
   * single task-file pass. During launch the live-status loop resumes every alive
   * member every audit cycle; doing that per member meant one synchronous
   * file-lock + read of every task file PER member PER cycle. This applies the
   * identical per-member resume logic against a member set in one locked pass, so
   * the mutations are exactly the same but the lock + reads happen once per cycle
   * instead of once per member. After a no-op pass, a task-file signature skips
   * unchanged repeat cycles without parsing every task JSON again.
   */
  resumeActiveIntervalsForMembers(
    teamName: string,
    memberNames: readonly string[],
    at = new Date().toISOString()
  ): ActivityIntervalResult {
    const memberKeys = new Set(
      memberNames.map((name) => normalizeMemberName(name)).filter((key): key is string => !!key)
    );
    if (memberKeys.size === 0) return { changedTasks: 0 };
    const memberKey = this.makeMemberSetKey(memberKeys);

    const cachedBeforeLock = this.resumeMembersCache.get(teamName);
    if (cachedBeforeLock?.memberKey === memberKey) {
      const beforeLockSignature = this.readTaskDirectorySignature(teamName);
      if (
        beforeLockSignature &&
        cachedBeforeLock.signatureKey === beforeLockSignature.key &&
        !fs.existsSync(this.getBoardStateLockPath(teamName))
      ) {
        return { changedTasks: 0 };
      }
    }

    const result = this.mutateTeamTasksWithLock(teamName, () => {
      const beforeSignature = this.readTaskDirectorySignature(teamName);
      const cached = this.resumeMembersCache.get(teamName);
      if (
        beforeSignature &&
        cached?.memberKey === memberKey &&
        cached.signatureKey === beforeSignature.key
      ) {
        return { changedTasks: 0 };
      }

      const mutationResult = this.mutateTeamTasksUnlocked(teamName, (task) => {
        let changed = false;

        if (
          task.status === 'in_progress' &&
          memberKeys.has(normalizeMemberName(task.owner)) &&
          !hasOpenWorkInterval(task)
        ) {
          const activeStartedAt = getActiveWorkStartedAt(task);
          task.workIntervals = [
            ...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
            { startedAt: resumeStartIso(activeStartedAt, at) },
          ];
          changed = true;
        }

        const activeReview = getActiveReviewStart(task);
        if (
          task.status === 'completed' &&
          activeReview &&
          memberKeys.has(normalizeMemberName(activeReview.reviewer)) &&
          !hasOpenReviewInterval(task, activeReview.reviewer)
        ) {
          task.reviewIntervals = [
            ...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
            {
              reviewer: activeReview.reviewer,
              startedAt: resumeStartIso(activeReview.startedAt, at),
            },
          ];
          changed = true;
        }

        return changed;
      });

      const nextSignature =
        mutationResult.changedTasks > 0
          ? this.readTaskDirectorySignature(teamName)
          : beforeSignature;
      if (nextSignature) {
        this.resumeMembersCache.set(teamName, {
          memberKey,
          signatureKey: nextSignature.key,
        });
      } else {
        this.resumeMembersCache.delete(teamName);
      }
      return mutationResult;
    });

    if (result.failed) {
      this.clearActivityNoopCachesForTeam(teamName);
    } else if (result.changedTasks > 0) {
      this.clearMemberActivityNoopCacheForTeam(teamName);
    }
    return result;
  }

  repairStaleIntervalsAfterCrash(
    teamName: string,
    launchSnapshot?: PersistedTeamLaunchSnapshot | null
  ): ActivityIntervalResult {
    const memberByName = new Map<string, PersistedTeamLaunchMemberState>();
    for (const member of Object.values(launchSnapshot?.members ?? {})) {
      memberByName.set(normalizeMemberName(member.name), member);
    }

    return this.mutateTeamTasks(teamName, (task) => {
      let changed = false;
      if (Array.isArray(task.workIntervals)) {
        const ownerMember = memberByName.get(normalizeMemberName(task.owner));
        task.workIntervals = task.workIntervals.map((interval) => {
          if (isClosedInterval(interval)) return interval;
          changed = true;
          return { ...interval, completedAt: crashRepairIntervalCloseIso(interval, ownerMember) };
        });
      }
      if (task.status === 'in_progress') {
        const ownerMember = memberByName.get(normalizeMemberName(task.owner));
        const startedAt = getActiveWorkStartedAt(task);
        if (
          startedAt &&
          !hasPersistedWorkIntervalAtOrAfter(task, startedAt) &&
          !hasWorkIntervalForStart(task, startedAt)
        ) {
          task.workIntervals = [
            ...(Array.isArray(task.workIntervals) ? task.workIntervals : []),
            { startedAt, completedAt: crashRepairCloseIso(startedAt, ownerMember) },
          ];
          changed = true;
        }
      }

      if (Array.isArray(task.reviewIntervals)) {
        task.reviewIntervals = task.reviewIntervals.map((interval) => {
          if (isClosedInterval(interval)) return interval;
          const reviewerMember = memberByName.get(normalizeMemberName(interval.reviewer));
          changed = true;
          return {
            ...interval,
            completedAt: crashRepairIntervalCloseIso(interval, reviewerMember),
          };
        });
      }
      if (task.status === 'completed') {
        const activeReview = getActiveReviewStart(task);
        if (
          activeReview &&
          !hasPersistedReviewIntervalAtOrAfter(task, activeReview.startedAt) &&
          !hasReviewIntervalForStart(task, activeReview.reviewer, activeReview.startedAt)
        ) {
          const reviewerMember = memberByName.get(normalizeMemberName(activeReview.reviewer));
          task.reviewIntervals = [
            ...(Array.isArray(task.reviewIntervals) ? task.reviewIntervals : []),
            {
              reviewer: activeReview.reviewer,
              startedAt: activeReview.startedAt,
              completedAt: crashRepairCloseIso(activeReview.startedAt, reviewerMember),
            },
          ];
          changed = true;
        }
      }

      return changed;
    });
  }
}
