import { FileReadTimeoutError, readFileUtf8WithTimeout } from '@main/utils/fsRead';
import { getTeamsBasePath } from '@main/utils/pathDecoder';
import { isLeadMember } from '@shared/utils/leadDetection';
import { createLogger } from '@shared/utils/logger';
import {
  createCliAutoSuffixNameGuard,
  createCliProvisionerNameGuard,
} from '@shared/utils/teamMemberName';
import * as fs from 'fs';
import * as path from 'path';

import { readBootstrapLaunchSnapshot } from './TeamBootstrapStateReader';
import { getTeamFsWorkerClient } from './TeamFsWorkerClient';
import { normalizePersistedLaunchSnapshot } from './TeamLaunchStateEvaluator';
import {
  choosePreferredLaunchStateSummary,
  type LaunchStateSummary,
  normalizePersistedLaunchSummaryProjection,
  shouldSuppressLegacyLaunchArtifactHeuristic,
  TEAM_LAUNCH_SUMMARY_FILE,
} from './TeamLaunchSummaryProjection';
import { TeamMembersMetaStore } from './TeamMembersMetaStore';
import { TeamMetaStore } from './TeamMetaStore';

import type {
  TeamConfig,
  TeamMember,
  TeamProviderId,
  TeamSummary,
  TeamSummaryMember,
} from '@shared/types';

const logger = createLogger('Service:TeamConfigReader');

const TEAM_LIST_CONCURRENCY = process.platform === 'win32' ? 4 : 12;
const LARGE_CONFIG_BYTES = 512 * 1024;
const CONFIG_HEAD_BYTES = 64 * 1024;
const MAX_CONFIG_READ_BYTES = 10 * 1024 * 1024; // 10MB hard limit for full config reads
const PER_TEAM_READ_TIMEOUT_MS = 5_000;
const GET_CONFIG_SLOW_READ_WARN_MS = 500;
const CONFIG_SNAPSHOT_RECENT_STAT_FAILURE_FALLBACK_MS = 5_000;
const COARSE_FS_FULL_VERIFY_MS = 1_500;
const LIST_TEAMS_CACHE_TTL_MS = 5_000;
const MAX_SESSION_HISTORY_IN_SUMMARY = 2000;
const MAX_PROJECT_PATH_HISTORY_IN_SUMMARY = 200;
const MAX_LAUNCH_STATE_BYTES = 32 * 1024;
const TEAM_LAUNCH_STATE_FILE = 'launch-state.json';

export interface TeamConfigFingerprint {
  size: string;
  mode: string;
  dev?: string;
  ino?: string;
  mtimeNs?: string;
  ctimeNs?: string;
  birthtimeNs?: string;
  mtimeMs: number;
  ctimeMs: number;
  birthtimeMs: number;
}

interface InternalTeamConfigFingerprint extends TeamConfigFingerprint {
  isFile: boolean;
  highResolution: boolean;
  numericSize: number;
}

interface CachedTeamConfig {
  value: TeamConfig;
  fingerprint: InternalTeamConfigFingerprint | null;
  verifiedAt: number;
  fullVerifiedAt: number;
}

type TeamConfigReadMode = 'verified' | 'snapshot';

interface ConfigReadTiming {
  teamName: string;
  mode: TeamConfigReadMode;
  configPath: string;
  size: number | null;
  statMs: number | null;
  readMs: number | null;
  parseMs: number | null;
  totalMs: number;
  likelyCause: string;
  fingerprintHighResolution: boolean | null;
  cacheGeneration: number | null;
  currentGeneration: number;
  caller: string | null;
}

interface CachedTeamList {
  value: TeamSummary[];
  expiresAt: number;
}

interface InFlightTeamList {
  promise: Promise<TeamSummary[]>;
  generationAtStart: number;
}

function normalizeProjectPathCandidate(value: unknown): string | undefined {
  if (typeof value !== 'string') {
    return undefined;
  }
  const trimmed = value.trim();
  return trimmed.length > 0 ? trimmed : undefined;
}

function resolveProjectPathFromConfig(
  config: Pick<TeamConfig, 'projectPath' | 'projectPathHistory' | 'members'>
): string | undefined {
  const direct = normalizeProjectPathCandidate(config.projectPath);
  if (direct) {
    return direct;
  }

  const leadMemberCwd = (config.members ?? []).find((member) => isLeadMember(member))?.cwd;
  const leadResolved = normalizeProjectPathCandidate(leadMemberCwd);
  if (leadResolved) {
    return leadResolved;
  }

  const distinctMemberCwds = Array.from(
    new Set(
      (config.members ?? [])
        .map((member) => normalizeProjectPathCandidate(member.cwd))
        .filter((cwd): cwd is string => Boolean(cwd))
    )
  );
  if (distinctMemberCwds.length === 1) {
    return distinctMemberCwds[0];
  }

  if (Array.isArray(config.projectPathHistory)) {
    for (let i = config.projectPathHistory.length - 1; i >= 0; i--) {
      const historyValue = normalizeProjectPathCandidate(config.projectPathHistory[i]);
      if (historyValue) {
        return historyValue;
      }
    }
  }

  return undefined;
}

async function readLaunchStateSummary(teamDir: string): Promise<LaunchStateSummary | null> {
  const bootstrapSnapshot = await readBootstrapLaunchSnapshot(path.basename(teamDir));
  const launchStatePath = path.join(teamDir, TEAM_LAUNCH_STATE_FILE);
  const launchSummaryPath = path.join(teamDir, TEAM_LAUNCH_SUMMARY_FILE);
  const [launchSnapshot, launchSummaryProjection] = await Promise.all([
    (async () => {
      try {
        const stat = await fs.promises.stat(launchStatePath);
        if (!stat.isFile() || stat.size > MAX_LAUNCH_STATE_BYTES) {
          return null;
        }

        const raw = await readFileUtf8WithTimeout(launchStatePath, PER_TEAM_READ_TIMEOUT_MS);
        return normalizePersistedLaunchSnapshot(path.basename(teamDir), JSON.parse(raw));
      } catch {
        return null;
      }
    })(),
    (async () => {
      try {
        const stat = await fs.promises.stat(launchSummaryPath);
        if (!stat.isFile() || stat.size > MAX_LAUNCH_STATE_BYTES) {
          return null;
        }
        const raw = await readFileUtf8WithTimeout(launchSummaryPath, PER_TEAM_READ_TIMEOUT_MS);
        return normalizePersistedLaunchSummaryProjection(path.basename(teamDir), JSON.parse(raw));
      } catch {
        return null;
      }
    })(),
  ]);

  return choosePreferredLaunchStateSummary({
    bootstrapSnapshot,
    launchSnapshot,
    launchSummaryProjection,
  });
}

async function mapLimit<T, R>(
  items: readonly T[],
  limit: number,
  fn: (item: T) => Promise<R>
): Promise<R[]> {
  const results = new Array<R>(items.length);
  let index = 0;
  const workerCount = Math.max(1, Math.min(limit, items.length));
  const workers = new Array(workerCount).fill(0).map(async () => {
    while (true) {
      const i = index++;
      if (i >= items.length) return;
      results[i] = await fn(items[i]);
    }
  });
  await Promise.all(workers);
  return results;
}

function withReadTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
  let timer: ReturnType<typeof setTimeout> | undefined;
  const timeout = new Promise<T>((_resolve, reject) => {
    timer = setTimeout(() => reject(new Error('Team config read timeout')), ms);
  });
  return Promise.race([promise, timeout]).finally(() => {
    if (timer) clearTimeout(timer);
  });
}

function cloneConfig(config: TeamConfig): TeamConfig {
  return structuredClone(config);
}

function cloneTeamSummaries(teams: readonly TeamSummary[]): TeamSummary[] {
  return structuredClone([...teams]);
}

function classifyConfigReadTiming(timing: {
  statMs: number | null;
  readMs: number | null;
  parseMs: number | null;
}): string {
  const statMs = timing.statMs ?? 0;
  const readMs = timing.readMs ?? 0;
  const parseMs = timing.parseMs ?? 0;
  if (readMs >= 1_000 && readMs >= statMs * 2 && readMs >= parseMs * 2) {
    return 'io_read_slow';
  }
  if (statMs >= 1_000 && statMs >= readMs * 2 && statMs >= parseMs * 2) {
    return 'io_stat_slow';
  }
  if (parseMs >= 500 && parseMs >= readMs && parseMs >= statMs) {
    return 'json_parse_slow';
  }
  if (statMs + readMs >= 1_000) {
    return 'filesystem_pressure';
  }
  return 'mixed_or_unknown';
}

function captureConfigReadCaller(): string | null {
  const stack = new Error().stack?.split('\n').slice(2) ?? [];
  const frame = stack.find((line) => {
    const normalized = line.trim();
    return (
      normalized.length > 0 &&
      !normalized.includes('TeamConfigReader.') &&
      !normalized.includes('TeamConfigReader.ts') &&
      !normalized.includes('captureConfigReadCaller') &&
      !normalized.includes('node:internal')
    );
  });
  return frame?.trim().slice(0, 240) ?? null;
}

export class TeamConfigReader {
  private static readonly configCacheByPath = new Map<string, CachedTeamConfig>();
  private static readonly configReadInFlightByPath = new Map<string, Promise<TeamConfig | null>>();
  private static readonly configStatInFlightByPath = new Map<
    string,
    Promise<InternalTeamConfigFingerprint | null>
  >();
  private static readonly configGenerationByPath = new Map<string, number>();
  private static readonly listTeamsCacheByBasePath = new Map<string, CachedTeamList>();
  private static readonly listTeamsInFlightByBasePath = new Map<string, InFlightTeamList>();
  private static listTeamsGeneration = 0;

  static clearCacheForTests(): void {
    TeamConfigReader.configCacheByPath.clear();
    TeamConfigReader.configReadInFlightByPath.clear();
    TeamConfigReader.configStatInFlightByPath.clear();
    TeamConfigReader.configGenerationByPath.clear();
    TeamConfigReader.listTeamsCacheByBasePath.clear();
    TeamConfigReader.listTeamsInFlightByBasePath.clear();
    TeamConfigReader.listTeamsGeneration = 0;
  }

  static invalidateTeam(teamName: string): void {
    const configPath = path.join(getTeamsBasePath(), teamName, 'config.json');
    TeamConfigReader.invalidatePath(configPath);
  }

  static invalidatePath(configPath: string): void {
    TeamConfigReader.configCacheByPath.delete(configPath);
    TeamConfigReader.configReadInFlightByPath.delete(configPath);
    TeamConfigReader.configStatInFlightByPath.delete(configPath);
    TeamConfigReader.bumpConfigGeneration(configPath);
    TeamConfigReader.invalidateListTeamsCache();
  }

  static invalidateListTeamsCache(): void {
    TeamConfigReader.listTeamsCacheByBasePath.clear();
    // Do not clear in-flight scans here. Config writes can arrive while a global
    // team scan is already running; dropping the in-flight entry starts a second
    // full scan over all teams and amplifies launch-time filesystem pressure.
    // The generation check below prevents the stale in-flight result from being
    // cached after invalidation.
    TeamConfigReader.listTeamsGeneration += 1;
  }

  private static invalidatePathForGeneration(
    configPath: string,
    expectedGeneration?: number
  ): void {
    if (
      typeof expectedGeneration === 'number' &&
      TeamConfigReader.getConfigGeneration(configPath) !== expectedGeneration
    ) {
      return;
    }
    TeamConfigReader.invalidatePath(configPath);
  }

  static async primeConfig(
    teamName: string,
    config: TeamConfig,
    fingerprint?: TeamConfigFingerprint | null
  ): Promise<void> {
    const configPath = path.join(getTeamsBasePath(), teamName, 'config.json');
    const generation = TeamConfigReader.bumpConfigGeneration(configPath);
    TeamConfigReader.configReadInFlightByPath.delete(configPath);
    TeamConfigReader.configStatInFlightByPath.delete(configPath);
    let internalFingerprint: InternalTeamConfigFingerprint | null = null;
    if (fingerprint) {
      internalFingerprint = {
        ...fingerprint,
        isFile: true,
        highResolution: Boolean(fingerprint.mtimeNs || fingerprint.ctimeNs),
        numericSize: Number(fingerprint.size),
      };
    } else {
      internalFingerprint = await TeamConfigReader.readConfigFingerprint(configPath).catch(
        () => null
      );
    }
    TeamConfigReader.storeConfigCache(configPath, config, internalFingerprint, true, generation);
    TeamConfigReader.invalidateListTeamsCache();
  }

  constructor(
    private readonly membersMetaStore: TeamMembersMetaStore = new TeamMembersMetaStore(),
    private readonly teamMetaStore: TeamMetaStore = new TeamMetaStore()
  ) {}

  async listTeams(): Promise<TeamSummary[]> {
    const teamsBasePath = getTeamsBasePath();
    const cached = TeamConfigReader.listTeamsCacheByBasePath.get(teamsBasePath);
    if (cached && cached.expiresAt > Date.now()) {
      return cloneTeamSummaries(cached.value);
    }

    const existingRequest = TeamConfigReader.listTeamsInFlightByBasePath.get(teamsBasePath);
    if (existingRequest?.generationAtStart === TeamConfigReader.listTeamsGeneration) {
      return cloneTeamSummaries(await existingRequest.promise);
    }

    const request = this.listTeamsUncached(teamsBasePath);
    const generationAtStart = TeamConfigReader.listTeamsGeneration;
    TeamConfigReader.listTeamsInFlightByBasePath.set(teamsBasePath, {
      promise: request,
      generationAtStart,
    });

    try {
      const teams = await request;
      if (TeamConfigReader.listTeamsGeneration === generationAtStart) {
        TeamConfigReader.listTeamsCacheByBasePath.set(teamsBasePath, {
          value: cloneTeamSummaries(teams),
          expiresAt: Date.now() + LIST_TEAMS_CACHE_TTL_MS,
        });
      }
      return cloneTeamSummaries(teams);
    } finally {
      if (TeamConfigReader.listTeamsInFlightByBasePath.get(teamsBasePath)?.promise === request) {
        TeamConfigReader.listTeamsInFlightByBasePath.delete(teamsBasePath);
      }
    }
  }

  private async listTeamsUncached(teamsBasePath: string): Promise<TeamSummary[]> {
    const worker = getTeamFsWorkerClient();
    if (worker.isAvailable()) {
      const startedAt = Date.now();
      try {
        const { teams, diag } = await worker.listTeams({
          largeConfigBytes: LARGE_CONFIG_BYTES,
          configHeadBytes: CONFIG_HEAD_BYTES,
          maxConfigBytes: MAX_CONFIG_READ_BYTES,
          maxMembersMetaBytes: 256 * 1024,
          maxSessionHistoryInSummary: MAX_SESSION_HISTORY_IN_SUMMARY,
          maxProjectPathHistoryInSummary: MAX_PROJECT_PATH_HISTORY_IN_SUMMARY,
          concurrency: TEAM_LIST_CONCURRENCY,
          maxConfigReadMs: PER_TEAM_READ_TIMEOUT_MS,
        });
        const ms = Date.now() - startedAt;
        const skipReasons =
          diag && typeof diag === 'object' ? (diag as Record<string, unknown>).skipReasons : null;
        if (skipReasons && typeof skipReasons === 'object') {
          const bad =
            Number((skipReasons as Record<string, unknown>).config_parse_failed ?? 0) +
            Number((skipReasons as Record<string, unknown>).config_read_timeout ?? 0);
          if (bad > 0) {
            logger.warn(`[listTeams] worker skipped broken team configs count=${bad}`);
          }
        }
        if (ms >= 1500) {
          logger.warn(`[listTeams] worker slow ms=${ms} diag=${JSON.stringify(diag)}`);
        }
        return teams;
      } catch (error) {
        logger.warn(
          `[listTeams] worker failed: ${error instanceof Error ? error.message : String(error)}`
        );
        // Fall through to in-process implementation.
      }
    }

    const teamsDir = teamsBasePath;

    let entries: fs.Dirent[];
    try {
      entries = await fs.promises.readdir(teamsDir, { withFileTypes: true });
    } catch {
      return [];
    }

    const teamDirs = entries.filter((e) => e.isDirectory());

    const perTeam: (TeamSummary | null)[] = await mapLimit(
      teamDirs,
      TEAM_LIST_CONCURRENCY,
      async (entry): Promise<TeamSummary | null> => {
        const teamName = entry.name;

        try {
          return await withReadTimeout(
            this.readTeamSummary(teamsDir, teamName),
            PER_TEAM_READ_TIMEOUT_MS
          );
        } catch (err) {
          const reason = err instanceof Error ? err.message : 'unknown';
          logger.warn(`Skipping team dir (${reason}): ${teamName}`);
          return null;
        }
      }
    );

    return perTeam.filter((t): t is TeamSummary => t !== null);
  }

  private async readTeamSummary(teamsDir: string, teamName: string): Promise<TeamSummary | null> {
    const configPath = path.join(teamsDir, teamName, 'config.json');
    const teamDir = path.join(teamsDir, teamName);

    try {
      let config: TeamConfig | null = null;
      let leadProviderId: TeamProviderId | undefined;
      let displayName: string | null = null;
      let description = '';
      let color: string | undefined;
      let projectPath: string | undefined;
      let leadSessionId: string | undefined;
      let deletedAt: string | undefined;
      let projectPathHistory: TeamConfig['projectPathHistory'] | undefined;
      let sessionHistory: TeamConfig['sessionHistory'] | undefined;

      let stat: fs.Stats | null = null;
      try {
        stat = await fs.promises.stat(configPath);
      } catch {
        stat = null;
      }

      // Skip non-regular files (pipes, sockets, etc.) — readFile could hang on them
      if (!stat?.isFile()) {
        // Fallback: check for draft team (team.meta.json without config.json)
        return this.readDraftTeamSummary(teamsDir, teamName);
      }

      // Safety: refuse to touch extremely large configs. Even "head" parsing can be misleading,
      // and full reads/parses can stall the main process.
      if (stat.size > MAX_CONFIG_READ_BYTES) {
        logger.warn(
          `Skipping team dir with oversized config.json (${stat.size} bytes): ${teamName}`
        );
        return null;
      }

      if (stat.size > LARGE_CONFIG_BYTES) {
        // Defensive: avoid any reads from very large configs during listing.
        // If the team is real, it can still be opened later via getConfig().
        displayName = teamName;
      } else {
        const raw = await readFileUtf8WithTimeout(configPath, PER_TEAM_READ_TIMEOUT_MS);
        config = JSON.parse(raw) as TeamConfig;
        displayName = typeof config.name === 'string' ? config.name : null;
        description = typeof config.description === 'string' ? config.description : '';
        color =
          typeof config.color === 'string' && config.color.trim().length > 0
            ? config.color
            : undefined;
        projectPath = resolveProjectPathFromConfig(config);
        leadSessionId =
          typeof config.leadSessionId === 'string' && config.leadSessionId.trim().length > 0
            ? config.leadSessionId
            : undefined;
        projectPathHistory = Array.isArray(config.projectPathHistory)
          ? config.projectPathHistory.slice(-MAX_PROJECT_PATH_HISTORY_IN_SUMMARY)
          : undefined;
        sessionHistory = Array.isArray(config.sessionHistory)
          ? config.sessionHistory.slice(-MAX_SESSION_HISTORY_IN_SUMMARY)
          : undefined;
        deletedAt = typeof config.deletedAt === 'string' ? config.deletedAt : undefined;
      }

      if (typeof displayName !== 'string' || displayName.trim() === '') {
        logger.debug(`Skipping team dir with invalid config name: ${teamName}`);
        return null;
      }

      // Case-insensitive dedup: key is lowercase name, value keeps the original casing
      const memberMap = new Map<string, TeamSummaryMember>();
      const removedKeys = new Set<string>();
      const expectedTeammateNames = new Set<string>();
      const confirmedArtifactNames = new Set<string>();
      let metaMembers: TeamMember[] = [];
      let leadName: string | undefined;
      let leadColor: string | undefined;

      const captureLeadMember = (m: TeamMember, overwrite = false): void => {
        if (m.removedAt) return;
        if (!isLeadMember(m)) return;
        const name = m.name?.trim();
        if (name && (overwrite || !leadName)) {
          leadName = name;
        }
        const colorValue = m.color?.trim();
        if (colorValue && (overwrite || !leadColor)) {
          leadColor = colorValue;
        }
      };

      const mergeMember = (m: TeamMember): void => {
        const name = m.name?.trim();
        if (!name) return;
        // Summary/memberCount should represent teammates (exclude the lead process).
        if (name === 'user' || isLeadMember(m)) return;
        const key = name.toLowerCase();
        // If meta marks this name removed, do not surface it in summaries
        if (removedKeys.has(key)) return;
        const existing = memberMap.get(key);
        memberMap.set(key, {
          name: existing?.name ?? name,
          role: m.role?.trim() || existing?.role,
          color: m.color?.trim() || existing?.color,
        });
      };

      // Also read members.meta.json — UI-created teams store members there,
      // and CLI-created teams may have additional members added via the UI.
      try {
        metaMembers = await this.membersMetaStore.getMembers(teamName);
        for (const member of metaMembers) {
          const name = member.name?.trim();
          if (!name) continue;
          captureLeadMember(member);
          // Summary/memberCount should represent teammates (exclude the lead process).
          if (name === 'user' || isLeadMember(member)) continue;
          const key = name.toLowerCase();
          if (member.removedAt) {
            removedKeys.add(key);
            continue;
          }
          expectedTeammateNames.add(name);
          mergeMember(member);
        }
      } catch {
        // best-effort — don't fail listing if meta file is broken
      }

      try {
        leadProviderId = (await this.teamMetaStore.getMeta(teamName))?.providerId;
      } catch {
        leadProviderId = undefined;
      }

      // Merge config members AFTER meta so removedAt can suppress stale config entries.
      if (config && Array.isArray(config.members)) {
        for (const member of config.members) {
          if (member && typeof member.name === 'string') {
            const name = member.name.trim();
            captureLeadMember(member, true);
            if (name && name !== 'user' && !isLeadMember(member)) {
              confirmedArtifactNames.add(name);
            }
            mergeMember(member);
          }
        }
      }

      try {
        const inboxDir = path.join(teamDir, 'inboxes');
        const inboxEntries = await fs.promises.readdir(inboxDir, { withFileTypes: true });
        for (const entry of inboxEntries) {
          if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
          const inboxName = entry.name.slice(0, -'.json'.length).trim();
          if (!inboxName || inboxName === 'user' || isLeadMember({ name: inboxName })) continue;
          confirmedArtifactNames.add(inboxName);
        }
      } catch {
        // best-effort
      }

      // Defense: drop CLI auto-suffixed duplicates (alice-2) only when the
      // base name is still active. Removed base members must not hide active
      // suffixed teammates in summary/list paths.
      const activeNamesForAutoSuffix = Array.from(memberMap.values())
        .map((member) => member.name)
        .filter((name) => !removedKeys.has(name.trim().toLowerCase()));
      const keepName = createCliAutoSuffixNameGuard(activeNamesForAutoSuffix);
      // Defense: drop CLI provisioner artifacts (alice-provisioner) when base name exists.
      const keepProvisioner = createCliProvisionerNameGuard(activeNamesForAutoSuffix);
      for (const [key, member] of Array.from(memberMap.entries())) {
        if (!keepName(member.name) || !keepProvisioner(member.name)) {
          memberMap.delete(key);
        }
      }

      const members = Array.from(memberMap.values());
      const suppressLegacyLaunchArtifactHeuristic = shouldSuppressLegacyLaunchArtifactHeuristic({
        leadProviderId,
        members: metaMembers,
      });
      const launchStateSummary =
        (await readLaunchStateSummary(teamDir)) ??
        (() => {
          if (suppressLegacyLaunchArtifactHeuristic) {
            return null;
          }
          if (
            !leadSessionId ||
            expectedTeammateNames.size === 0 ||
            confirmedArtifactNames.size === 0
          ) {
            return null;
          }
          const missingMembers = Array.from(expectedTeammateNames).filter(
            (name) => !confirmedArtifactNames.has(name)
          );
          if (missingMembers.length === 0) {
            return null;
          }
          return {
            partialLaunchFailure: true as const,
            expectedMemberCount: expectedTeammateNames.size,
            confirmedMemberCount: confirmedArtifactNames.size,
            missingMembers,
          };
        })();
      const summary: TeamSummary = {
        teamName,
        displayName,
        description,
        memberCount: memberMap.size,
        taskCount: 0,
        lastActivity: null,
        ...(members.length > 0 ? { members } : {}),
        ...(leadName ? { leadName } : {}),
        ...(leadColor ? { leadColor } : {}),
        ...(color ? { color } : {}),
        ...(projectPath ? { projectPath } : {}),
        ...(leadSessionId ? { leadSessionId } : {}),
        ...(projectPathHistory ? { projectPathHistory } : {}),
        ...(sessionHistory ? { sessionHistory } : {}),
        ...(deletedAt ? { deletedAt } : {}),
        ...(launchStateSummary ?? {}),
      };
      return summary;
    } catch {
      logger.debug(`Skipping team dir without valid config: ${teamName}`);
      return null;
    }
  }

  /**
   * Checks for a draft team (team.meta.json exists without config.json).
   * This happens when provisioning failed before CLI's TeamCreate could run.
   */
  private async readDraftTeamSummary(
    teamsDir: string,
    teamName: string
  ): Promise<TeamSummary | null> {
    const metaPath = path.join(teamsDir, teamName, 'team.meta.json');
    try {
      const metaStat = await fs.promises.stat(metaPath);
      if (!metaStat.isFile() || metaStat.size > 256 * 1024) {
        return null;
      }
      const metaRaw = await readFileUtf8WithTimeout(metaPath, PER_TEAM_READ_TIMEOUT_MS);
      const meta = JSON.parse(metaRaw) as Record<string, unknown>;
      if (meta?.version !== 1 || typeof meta?.cwd !== 'string') {
        return null;
      }

      const displayName =
        typeof meta.displayName === 'string' && meta.displayName.trim()
          ? meta.displayName.trim()
          : teamName;

      let memberCount = 0;
      let leadName: string | undefined;
      let leadColor: string | undefined;
      try {
        const members = await this.membersMetaStore.getMembers(teamName);
        memberCount = members.filter((member) => {
          const name = member.name?.trim() ?? '';
          if (!member.removedAt && isLeadMember(member)) {
            if (name) {
              leadName = name;
            }
            const color = member.color?.trim();
            if (color) {
              leadColor = color;
            }
          }
          if (!name || name === 'user' || isLeadMember(member)) {
            return false;
          }
          return !member.removedAt;
        }).length;
      } catch {
        // best-effort
      }

      return {
        teamName,
        displayName,
        description: typeof meta.description === 'string' ? meta.description : '',
        memberCount,
        taskCount: 0,
        lastActivity:
          typeof meta.createdAt === 'number' ? new Date(meta.createdAt).toISOString() : null,
        color: typeof meta.color === 'string' ? meta.color : undefined,
        ...(leadName ? { leadName } : {}),
        ...(leadColor ? { leadColor } : {}),
        projectPath: typeof meta.cwd === 'string' ? meta.cwd : undefined,
        pendingCreate: true,
      };
    } catch {
      return null;
    }
  }

  async getConfig(teamName: string): Promise<TeamConfig | null> {
    return this.getConfigVerified(teamName);
  }

  async getConfigVerified(teamName: string): Promise<TeamConfig | null> {
    const configPath = path.join(getTeamsBasePath(), teamName, 'config.json');
    const existingRead = TeamConfigReader.configReadInFlightByPath.get(configPath);
    if (existingRead) {
      return this.resolveConfigRead(teamName, configPath, existingRead);
    }

    const generation = TeamConfigReader.getConfigGeneration(configPath);
    const readPromise = this.readConfigFromDisk(
      teamName,
      configPath,
      null,
      true,
      generation,
      'verified'
    );
    TeamConfigReader.configReadInFlightByPath.set(configPath, readPromise);

    try {
      return await this.resolveConfigRead(teamName, configPath, readPromise);
    } finally {
      if (TeamConfigReader.configReadInFlightByPath.get(configPath) === readPromise) {
        TeamConfigReader.configReadInFlightByPath.delete(configPath);
      }
    }
  }

  async getConfigSnapshot(teamName: string): Promise<TeamConfig | null> {
    const configPath = path.join(getTeamsBasePath(), teamName, 'config.json');

    for (let attempt = 0; attempt < 3; attempt++) {
      const generationAtStart = TeamConfigReader.getConfigGeneration(configPath);
      let fingerprint: InternalTeamConfigFingerprint | null;

      try {
        fingerprint = await TeamConfigReader.getConfigFingerprint(configPath);
      } catch (error) {
        if (TeamConfigReader.getConfigGeneration(configPath) !== generationAtStart) {
          continue;
        }
        const cached = TeamConfigReader.configCacheByPath.get(configPath);
        if (
          cached &&
          Date.now() - cached.verifiedAt <= CONFIG_SNAPSHOT_RECENT_STAT_FAILURE_FALLBACK_MS
        ) {
          logger.warn(
            `[getConfigSnapshot] config_snapshot_stat_failed_using_recent_cache team=${teamName} error=${
              error instanceof Error ? error.message : String(error)
            }`
          );
          return cloneConfig(cached.value);
        }
        return null;
      }

      if (TeamConfigReader.getConfigGeneration(configPath) !== generationAtStart) {
        continue;
      }

      if (!fingerprint?.isFile || fingerprint.numericSize > MAX_CONFIG_READ_BYTES) {
        TeamConfigReader.invalidatePathForGeneration(configPath, generationAtStart);
        if (fingerprint && fingerprint.numericSize > MAX_CONFIG_READ_BYTES) {
          logger.warn(
            `Refusing to load oversized config.json (${fingerprint.numericSize} bytes) for team: ${teamName}`
          );
        }
        return null;
      }

      const cached = TeamConfigReader.configCacheByPath.get(configPath);
      if (
        cached?.fingerprint &&
        TeamConfigReader.fingerprintsEqual(cached.fingerprint, fingerprint)
      ) {
        const now = Date.now();
        const mustRevalidateCoarseFingerprint =
          !fingerprint.highResolution && now - cached.fullVerifiedAt >= COARSE_FS_FULL_VERIFY_MS;
        if (!mustRevalidateCoarseFingerprint) {
          cached.verifiedAt = now;
          return cloneConfig(cached.value);
        }
      }

      const existingRead = TeamConfigReader.configReadInFlightByPath.get(configPath);
      if (existingRead) {
        return this.resolveConfigRead(teamName, configPath, existingRead);
      }

      const generation = TeamConfigReader.getConfigGeneration(configPath);
      const readPromise = this.readConfigFromDisk(
        teamName,
        configPath,
        fingerprint,
        true,
        generation,
        'snapshot'
      );
      TeamConfigReader.configReadInFlightByPath.set(configPath, readPromise);
      try {
        return await this.resolveConfigRead(teamName, configPath, readPromise);
      } finally {
        if (TeamConfigReader.configReadInFlightByPath.get(configPath) === readPromise) {
          TeamConfigReader.configReadInFlightByPath.delete(configPath);
        }
      }
    }

    return null;
  }

  private async resolveConfigRead(
    teamName: string,
    configPath: string,
    readPromise: Promise<TeamConfig | null>
  ): Promise<TeamConfig | null> {
    try {
      const config = await readPromise;
      return config ? cloneConfig(config) : null;
    } catch {
      return null;
    }
  }

  private static async getConfigFingerprint(
    configPath: string
  ): Promise<InternalTeamConfigFingerprint | null> {
    const existing = TeamConfigReader.configStatInFlightByPath.get(configPath);
    if (existing) return existing;

    const statPromise = TeamConfigReader.readConfigFingerprint(configPath).finally(() => {
      if (TeamConfigReader.configStatInFlightByPath.get(configPath) === statPromise) {
        TeamConfigReader.configStatInFlightByPath.delete(configPath);
      }
    });
    TeamConfigReader.configStatInFlightByPath.set(configPath, statPromise);
    return statPromise;
  }

  private static async readConfigFingerprint(
    configPath: string
  ): Promise<InternalTeamConfigFingerprint | null> {
    let stat: fs.BigIntStats;
    try {
      stat = await withReadTimeout(
        fs.promises.stat(configPath, { bigint: true }),
        PER_TEAM_READ_TIMEOUT_MS
      );
    } catch (error) {
      const code = typeof error === 'object' && error ? (error as { code?: unknown }).code : null;
      if (code === 'ENOENT') {
        return null;
      }
      throw error;
    }

    const highResStat = stat as fs.BigIntStats & {
      mtimeNs?: bigint;
      ctimeNs?: bigint;
      birthtimeNs?: bigint;
    };
    const mtimeNs = highResStat.mtimeNs;
    const ctimeNs = highResStat.ctimeNs;
    const birthtimeNs = highResStat.birthtimeNs;

    return {
      size: stat.size.toString(),
      mode: stat.mode.toString(),
      dev: stat.dev.toString(),
      ino: stat.ino.toString(),
      mtimeNs: typeof mtimeNs === 'bigint' ? mtimeNs.toString() : undefined,
      ctimeNs: typeof ctimeNs === 'bigint' ? ctimeNs.toString() : undefined,
      birthtimeNs: typeof birthtimeNs === 'bigint' ? birthtimeNs.toString() : undefined,
      mtimeMs: Number(stat.mtimeMs),
      ctimeMs: Number(stat.ctimeMs),
      birthtimeMs: Number(stat.birthtimeMs),
      isFile: stat.isFile(),
      highResolution: typeof mtimeNs === 'bigint' || typeof ctimeNs === 'bigint',
      numericSize: Number(stat.size),
    };
  }

  private static fingerprintsEqual(
    a: InternalTeamConfigFingerprint,
    b: InternalTeamConfigFingerprint
  ): boolean {
    return (
      a.size === b.size &&
      a.mode === b.mode &&
      a.dev === b.dev &&
      a.ino === b.ino &&
      a.mtimeNs === b.mtimeNs &&
      a.ctimeNs === b.ctimeNs &&
      a.birthtimeNs === b.birthtimeNs &&
      a.mtimeMs === b.mtimeMs &&
      a.ctimeMs === b.ctimeMs &&
      a.birthtimeMs === b.birthtimeMs
    );
  }

  private static storeConfigCache(
    configPath: string,
    config: TeamConfig,
    fingerprint: InternalTeamConfigFingerprint | null,
    fullVerified: boolean,
    expectedGeneration?: number
  ): void {
    if (
      typeof expectedGeneration === 'number' &&
      TeamConfigReader.getConfigGeneration(configPath) !== expectedGeneration
    ) {
      return;
    }
    const now = Date.now();
    const previous = TeamConfigReader.configCacheByPath.get(configPath);
    TeamConfigReader.configCacheByPath.set(configPath, {
      value: cloneConfig(config),
      fingerprint,
      verifiedAt: now,
      fullVerifiedAt: fullVerified ? now : (previous?.fullVerifiedAt ?? now),
    });
  }

  private static getConfigGeneration(configPath: string): number {
    return TeamConfigReader.configGenerationByPath.get(configPath) ?? 0;
  }

  private static bumpConfigGeneration(configPath: string): number {
    const next = TeamConfigReader.getConfigGeneration(configPath) + 1;
    TeamConfigReader.configGenerationByPath.set(configPath, next);
    return next;
  }

  private async readConfigFromDisk(
    teamName: string,
    configPath: string,
    knownFingerprint: InternalTeamConfigFingerprint | null = null,
    updateCache = false,
    cacheGeneration?: number,
    mode: TeamConfigReadMode = 'verified'
  ): Promise<TeamConfig | null> {
    const startedAt = performance.now();
    const caller = captureConfigReadCaller();
    let size: number | null = null;
    let statMs: number | null = null;
    let readMs: number | null = null;
    let parseMs: number | null = null;
    let fingerprintHighResolution: boolean | null = knownFingerprint?.highResolution ?? null;

    const buildTiming = (): ConfigReadTiming => ({
      teamName,
      mode,
      configPath,
      size,
      statMs,
      readMs,
      parseMs,
      totalMs: Math.round(performance.now() - startedAt),
      likelyCause: classifyConfigReadTiming({ statMs, readMs, parseMs }),
      fingerprintHighResolution,
      cacheGeneration: cacheGeneration ?? null,
      currentGeneration: TeamConfigReader.getConfigGeneration(configPath),
      caller,
    });

    try {
      const statStartedAt = performance.now();
      const fingerprint =
        knownFingerprint ?? (await TeamConfigReader.getConfigFingerprint(configPath));
      statMs = Math.round(performance.now() - statStartedAt);
      size = fingerprint?.numericSize ?? null;
      fingerprintHighResolution = fingerprint?.highResolution ?? null;

      // Safety: refuse special files and huge/binary configs
      if (!fingerprint?.isFile) {
        TeamConfigReader.invalidatePathForGeneration(configPath, cacheGeneration);
        return null;
      }
      if (fingerprint.numericSize > MAX_CONFIG_READ_BYTES) {
        TeamConfigReader.invalidatePathForGeneration(configPath, cacheGeneration);
        logger.warn(
          `Refusing to load oversized config.json (${fingerprint.numericSize} bytes) for team: ${teamName}`
        );
        return null;
      }

      const readStartedAt = performance.now();
      const raw = await readFileUtf8WithTimeout(configPath, PER_TEAM_READ_TIMEOUT_MS);
      readMs = Math.round(performance.now() - readStartedAt);

      const parseStartedAt = performance.now();
      const config = JSON.parse(raw) as TeamConfig;
      parseMs = Math.round(performance.now() - parseStartedAt);
      if (typeof config.name !== 'string' || config.name.trim() === '') {
        TeamConfigReader.invalidatePathForGeneration(configPath, cacheGeneration);
        return null;
      }
      const resolvedProjectPath = resolveProjectPathFromConfig(config);
      const resolvedConfig = resolvedProjectPath
        ? { ...config, projectPath: resolvedProjectPath }
        : config;

      const totalMs = performance.now() - startedAt;
      if (totalMs >= GET_CONFIG_SLOW_READ_WARN_MS) {
        logger.warn(`[getConfig] slow read diag=${JSON.stringify(buildTiming())}`);
      }
      if (updateCache) {
        TeamConfigReader.storeConfigCache(
          configPath,
          resolvedConfig,
          fingerprint,
          true,
          cacheGeneration
        );
      }
      return resolvedConfig;
    } catch (error) {
      TeamConfigReader.invalidatePathForGeneration(configPath, cacheGeneration);
      if (error instanceof FileReadTimeoutError) {
        logger.warn(`[getConfig] ${error.message} diag=${JSON.stringify(buildTiming())}`);
      } else if (error instanceof Error && error.message === 'Team config read timeout') {
        logger.warn(
          `[getConfig] Timed out after ${PER_TEAM_READ_TIMEOUT_MS}ms reading ${configPath} diag=${JSON.stringify(buildTiming())}`
        );
      }
      throw error;
    }
  }

  async updateConfig(
    teamName: string,
    updates: { name?: string; description?: string; color?: string; language?: string }
  ): Promise<TeamConfig | null> {
    const config = await this.getConfig(teamName);
    if (!config) {
      return null;
    }
    if (updates.name !== undefined && updates.name.trim() !== '') {
      config.name = updates.name.trim();
    }
    if (updates.description !== undefined) {
      config.description = updates.description.trim() || undefined;
    }
    if (updates.color !== undefined) {
      config.color = updates.color.trim() || undefined;
    }
    if (updates.language !== undefined) {
      config.language = updates.language.trim() || undefined;
    }
    const configPath = path.join(getTeamsBasePath(), teamName, 'config.json');
    await fs.promises.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
    await TeamConfigReader.primeConfig(teamName, config);
    return config;
  }
}
