import { setCurrentMainOp } from '@main/services/infrastructure/EventLoopLagMonitor';
import { getAppIconPath } from '@main/utils/appIcon';
import { stripMarkdown } from '@main/utils/textFormatting';
import {
  TEAM_ADD_MEMBER,
  TEAM_ADD_TASK_COMMENT,
  TEAM_ADD_TASK_RELATIONSHIP,
  TEAM_ALIVE_LIST,
  TEAM_CANCEL_PROVISIONING,
  TEAM_CREATE,
  TEAM_CREATE_CONFIG,
  TEAM_CREATE_TASK,
  TEAM_DELETE_TASK_ATTACHMENT,
  TEAM_DELETE_TEAM,
  TEAM_GET_ALL_TASKS,
  TEAM_GET_ATTACHMENTS,
  TEAM_GET_CLAUDE_LOGS,
  TEAM_GET_DATA,
  TEAM_GET_DELETED_TASKS,
  TEAM_GET_LOGS_FOR_TASK,
  TEAM_GET_MEMBER_LOGS,
  TEAM_GET_MEMBER_STATS,
  TEAM_GET_PROJECT_BRANCH,
  TEAM_GET_TASK_ATTACHMENT,
  TEAM_KILL_PROCESS,
  TEAM_LAUNCH,
  TEAM_LEAD_ACTIVITY,
  TEAM_LEAD_CONTEXT,
  TEAM_MEMBER_SPAWN_STATUSES,
  TEAM_LIST,
  TEAM_PERMANENTLY_DELETE,
  TEAM_PREPARE_PROVISIONING,
  TEAM_PROCESS_ALIVE,
  TEAM_PROCESS_SEND,
  TEAM_PROVISIONING_PROGRESS,
  TEAM_PROVISIONING_STATUS,
  TEAM_REMOVE_MEMBER,
  TEAM_REMOVE_TASK_RELATIONSHIP,
  TEAM_REPLACE_MEMBERS,
  TEAM_REQUEST_REVIEW,
  TEAM_RESTORE,
  TEAM_RESTORE_TASK,
  TEAM_SAVE_TASK_ATTACHMENT,
  TEAM_SEND_MESSAGE,
  TEAM_SET_TASK_CLARIFICATION,
  TEAM_SHOW_MESSAGE_NOTIFICATION,
  TEAM_SOFT_DELETE_TASK,
  TEAM_START_TASK,
  TEAM_STOP,
  TEAM_TOOL_APPROVAL_RESPOND,
  TEAM_TOOL_APPROVAL_SETTINGS,
  TEAM_UPDATE_CONFIG,
  TEAM_UPDATE_KANBAN,
  TEAM_UPDATE_KANBAN_COLUMN_ORDER,
  TEAM_UPDATE_MEMBER_ROLE,
  TEAM_UPDATE_TASK_FIELDS,
  TEAM_UPDATE_TASK_OWNER,
  TEAM_UPDATE_TASK_STATUS,
  TEAM_VALIDATE_CLI_ARGS,
  // eslint-disable-next-line boundaries/element-types -- IPC channel constants are shared between main and preload by design
} from '@preload/constants/ipcChannels';
import { AGENT_BLOCK_CLOSE, AGENT_BLOCK_OPEN } from '@shared/constants/agentBlocks';
import { KANBAN_COLUMN_IDS } from '@shared/constants/kanban';
import { MAX_TEXT_LENGTH } from '@shared/constants/teamLimits';
import {
  extractFlagsFromHelp,
  extractUserFlags,
  PROTECTED_CLI_FLAGS,
} from '@shared/utils/cliArgsParser';
import { createLogger } from '@shared/utils/logger';
import { isRateLimitMessage } from '@shared/utils/rateLimitDetector';
import { BrowserWindow, type IpcMain, type IpcMainInvokeEvent, Notification } from 'electron';
import * as fs from 'fs';
import * as path from 'path';

import { ConfigManager } from '../services/infrastructure/ConfigManager';
import { NotificationManager } from '../services/infrastructure/NotificationManager';
import {
  buildActionModeAgentBlock,
  isAgentActionMode,
} from '../services/team/actionModeInstructions';
import { gitIdentityResolver } from '../services/parsing/GitIdentityResolver';
import { TeamAttachmentStore } from '../services/team/TeamAttachmentStore';
import { TeamTaskAttachmentStore } from '../services/team/TeamTaskAttachmentStore';

import {
  validateFromField,
  validateMemberName,
  validateTaskId,
  validateTeammateName,
  validateTeamName,
} from './guards';

import type {
  MemberStatsComputer,
  TeamDataService,
  TeamMemberLogsFinder,
  TeamProvisioningService,
} from '../services';
import type {
  AgentActionMode,
  AttachmentFileData,
  AttachmentMeta,
  AttachmentPayload,
  CreateTaskRequest,
  EffortLevel,
  GlobalTask,
  IpcResult,
  KanbanColumnId,
  LeadContextUsage,
  MemberFullStats,
  MemberLogSummary,
  MemberSpawnStatusEntry,
  SendMessageRequest,
  SendMessageResult,
  TaskAttachmentMeta,
  TaskComment,
  TeamClaudeLogsQuery,
  TeamClaudeLogsResponse,
  TeamConfig,
  TeamCreateConfigRequest,
  TeamCreateRequest,
  TeamCreateResponse,
  TeamData,
  TeamLaunchRequest,
  TeamLaunchResponse,
  TeamMessageNotificationData,
  TeamProvisioningPrepareResult,
  TeamProvisioningProgress,
  TeamSummary,
  TeamTask,
  TeamTaskStatus,
  TeamUpdateConfigRequest,
  ToolApprovalSettings,
  UpdateKanbanPatch,
} from '@shared/types';
import type { CliArgsValidationResult } from '@shared/utils/cliArgsParser';

const logger = createLogger('IPC:teams');

/**
 * In-memory set of rate-limit message keys already processed.
 * Independent of NotificationManager storage — survives notification deletion/pruning.
 * Without this, deleted rate-limit notifications would re-appear on next getData() scan.
 */
const seenRateLimitKeys = new Set<string>();
const SEEN_RATE_LIMIT_KEYS_MAX = 500;

/**
 * Check messages for rate limit indicators and fire notifications for new ones.
 * Uses both in-memory seenRateLimitKeys (to prevent resurrection after deletion)
 * and NotificationManager dedupeKey (to prevent storage duplicates).
 */
function checkRateLimitMessages(
  messages: readonly { messageId?: string; from: string; text: string; timestamp: string }[],
  teamName: string,
  teamDisplayName: string,
  projectPath?: string
): void {
  for (const msg of messages) {
    if (msg.from === 'user') continue;
    if (!isRateLimitMessage(msg.text)) continue;

    const rawKey = msg.messageId ?? `${msg.from}:${msg.timestamp}`;
    const dedupeKey = `rate-limit:${teamName}:${rawKey}`;

    // In-memory guard: prevents resurrection after user deletes the notification
    if (seenRateLimitKeys.has(dedupeKey)) continue;
    seenRateLimitKeys.add(dedupeKey);

    // Evict oldest entries to prevent unbounded growth
    if (seenRateLimitKeys.size > SEEN_RATE_LIMIT_KEYS_MAX) {
      const first = seenRateLimitKeys.values().next().value;
      if (first) seenRateLimitKeys.delete(first);
    }

    void NotificationManager.getInstance()
      .addTeamNotification({
        teamEventType: 'rate_limit',
        teamName,
        teamDisplayName,
        from: msg.from,
        summary: `Rate limit: ${msg.from}`,
        body: msg.text.slice(0, 200),
        dedupeKey,
        projectPath,
      })
      .catch(() => undefined);
  }
}

let teamDataService: TeamDataService | null = null;
let teamProvisioningService: TeamProvisioningService | null = null;
let teamMemberLogsFinder: TeamMemberLogsFinder | null = null;
let memberStatsComputer: MemberStatsComputer | null = null;

const attachmentStore = new TeamAttachmentStore();
const taskAttachmentStore = new TeamTaskAttachmentStore();

const ALLOWED_ATTACHMENT_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
const MAX_ATTACHMENT_SIZE = 10 * 1024 * 1024; // 10MB per file
const MAX_ATTACHMENTS = 5;
const MAX_TOTAL_ATTACHMENT_SIZE = 20 * 1024 * 1024; // 20MB total

export function initializeTeamHandlers(
  service: TeamDataService,
  provisioningService: TeamProvisioningService,
  logsFinder?: TeamMemberLogsFinder,
  statsComputer?: MemberStatsComputer
): void {
  teamDataService = service;
  teamProvisioningService = provisioningService;
  teamMemberLogsFinder = logsFinder ?? null;
  memberStatsComputer = statsComputer ?? null;
}

export function registerTeamHandlers(ipcMain: IpcMain): void {
  ipcMain.handle(TEAM_LIST, handleListTeams);
  ipcMain.handle(TEAM_GET_DATA, handleGetData);
  ipcMain.handle(TEAM_GET_CLAUDE_LOGS, handleGetClaudeLogs);
  ipcMain.handle(TEAM_PREPARE_PROVISIONING, handlePrepareProvisioning);
  ipcMain.handle(TEAM_CREATE, handleCreateTeam);
  ipcMain.handle(TEAM_LAUNCH, handleLaunchTeam);
  ipcMain.handle(TEAM_PROVISIONING_STATUS, handleProvisioningStatus);
  ipcMain.handle(TEAM_CANCEL_PROVISIONING, handleCancelProvisioning);
  ipcMain.handle(TEAM_SEND_MESSAGE, handleSendMessage);
  ipcMain.handle(TEAM_CREATE_TASK, handleCreateTask);
  ipcMain.handle(TEAM_REQUEST_REVIEW, handleRequestReview);
  ipcMain.handle(TEAM_UPDATE_KANBAN, handleUpdateKanban);
  ipcMain.handle(TEAM_UPDATE_KANBAN_COLUMN_ORDER, handleUpdateKanbanColumnOrder);
  ipcMain.handle(TEAM_UPDATE_TASK_STATUS, handleUpdateTaskStatus);
  ipcMain.handle(TEAM_UPDATE_TASK_OWNER, handleUpdateTaskOwner);
  ipcMain.handle(TEAM_UPDATE_TASK_FIELDS, handleUpdateTaskFields);
  ipcMain.handle(TEAM_DELETE_TEAM, handleDeleteTeam);
  ipcMain.handle(TEAM_RESTORE, handleRestoreTeam);
  ipcMain.handle(TEAM_PERMANENTLY_DELETE, handlePermanentlyDeleteTeam);
  ipcMain.handle(TEAM_PROCESS_SEND, handleProcessSend);
  ipcMain.handle(TEAM_PROCESS_ALIVE, handleProcessAlive);
  ipcMain.handle(TEAM_ALIVE_LIST, handleAliveList);
  ipcMain.handle(TEAM_STOP, handleStopTeam);
  ipcMain.handle(TEAM_CREATE_CONFIG, handleCreateConfig);
  ipcMain.handle(TEAM_GET_MEMBER_LOGS, handleGetMemberLogs);
  ipcMain.handle(TEAM_GET_LOGS_FOR_TASK, handleGetLogsForTask);
  ipcMain.handle(TEAM_GET_MEMBER_STATS, handleGetMemberStats);
  ipcMain.handle(TEAM_UPDATE_CONFIG, handleUpdateConfig);
  ipcMain.handle(TEAM_START_TASK, handleStartTask);
  ipcMain.handle(TEAM_GET_ALL_TASKS, handleGetAllTasks);
  ipcMain.handle(TEAM_ADD_TASK_COMMENT, handleAddTaskComment);
  ipcMain.handle(TEAM_ADD_MEMBER, handleAddMember);
  ipcMain.handle(TEAM_REPLACE_MEMBERS, handleReplaceMembers);
  ipcMain.handle(TEAM_REMOVE_MEMBER, handleRemoveMember);
  ipcMain.handle(TEAM_UPDATE_MEMBER_ROLE, handleUpdateMemberRole);
  ipcMain.handle(TEAM_GET_PROJECT_BRANCH, handleGetProjectBranch);
  ipcMain.handle(TEAM_GET_ATTACHMENTS, handleGetAttachments);
  ipcMain.handle(TEAM_KILL_PROCESS, handleKillProcess);
  ipcMain.handle(TEAM_LEAD_ACTIVITY, handleLeadActivity);
  ipcMain.handle(TEAM_LEAD_CONTEXT, handleLeadContext);
  ipcMain.handle(TEAM_MEMBER_SPAWN_STATUSES, handleMemberSpawnStatuses);
  ipcMain.handle(TEAM_SOFT_DELETE_TASK, handleSoftDeleteTask);
  ipcMain.handle(TEAM_RESTORE_TASK, handleRestoreTask);
  ipcMain.handle(TEAM_GET_DELETED_TASKS, handleGetDeletedTasks);
  ipcMain.handle(TEAM_SET_TASK_CLARIFICATION, handleSetTaskClarification);
  ipcMain.handle(TEAM_SHOW_MESSAGE_NOTIFICATION, handleShowMessageNotification);
  ipcMain.handle(TEAM_ADD_TASK_RELATIONSHIP, handleAddTaskRelationship);
  ipcMain.handle(TEAM_REMOVE_TASK_RELATIONSHIP, handleRemoveTaskRelationship);
  ipcMain.handle(TEAM_SAVE_TASK_ATTACHMENT, handleSaveTaskAttachment);
  ipcMain.handle(TEAM_GET_TASK_ATTACHMENT, handleGetTaskAttachment);
  ipcMain.handle(TEAM_DELETE_TASK_ATTACHMENT, handleDeleteTaskAttachment);
  ipcMain.handle(TEAM_TOOL_APPROVAL_RESPOND, handleToolApprovalRespond);
  ipcMain.handle(TEAM_VALIDATE_CLI_ARGS, handleValidateCliArgs);
  ipcMain.handle(TEAM_TOOL_APPROVAL_SETTINGS, handleToolApprovalSettings);
  logger.info('Team handlers registered');
}

export function removeTeamHandlers(ipcMain: IpcMain): void {
  ipcMain.removeHandler(TEAM_LIST);
  ipcMain.removeHandler(TEAM_GET_DATA);
  ipcMain.removeHandler(TEAM_GET_CLAUDE_LOGS);
  ipcMain.removeHandler(TEAM_PREPARE_PROVISIONING);
  ipcMain.removeHandler(TEAM_CREATE);
  ipcMain.removeHandler(TEAM_LAUNCH);
  ipcMain.removeHandler(TEAM_PROVISIONING_STATUS);
  ipcMain.removeHandler(TEAM_CANCEL_PROVISIONING);
  ipcMain.removeHandler(TEAM_SEND_MESSAGE);
  ipcMain.removeHandler(TEAM_CREATE_TASK);
  ipcMain.removeHandler(TEAM_REQUEST_REVIEW);
  ipcMain.removeHandler(TEAM_UPDATE_KANBAN);
  ipcMain.removeHandler(TEAM_UPDATE_KANBAN_COLUMN_ORDER);
  ipcMain.removeHandler(TEAM_UPDATE_TASK_STATUS);
  ipcMain.removeHandler(TEAM_UPDATE_TASK_OWNER);
  ipcMain.removeHandler(TEAM_UPDATE_TASK_FIELDS);
  ipcMain.removeHandler(TEAM_DELETE_TEAM);
  ipcMain.removeHandler(TEAM_RESTORE);
  ipcMain.removeHandler(TEAM_PERMANENTLY_DELETE);
  ipcMain.removeHandler(TEAM_PROCESS_SEND);
  ipcMain.removeHandler(TEAM_PROCESS_ALIVE);
  ipcMain.removeHandler(TEAM_ALIVE_LIST);
  ipcMain.removeHandler(TEAM_STOP);
  ipcMain.removeHandler(TEAM_CREATE_CONFIG);
  ipcMain.removeHandler(TEAM_GET_MEMBER_LOGS);
  ipcMain.removeHandler(TEAM_GET_LOGS_FOR_TASK);
  ipcMain.removeHandler(TEAM_GET_MEMBER_STATS);
  ipcMain.removeHandler(TEAM_UPDATE_CONFIG);
  ipcMain.removeHandler(TEAM_START_TASK);
  ipcMain.removeHandler(TEAM_GET_ALL_TASKS);
  ipcMain.removeHandler(TEAM_ADD_TASK_COMMENT);
  ipcMain.removeHandler(TEAM_ADD_MEMBER);
  ipcMain.removeHandler(TEAM_REPLACE_MEMBERS);
  ipcMain.removeHandler(TEAM_REMOVE_MEMBER);
  ipcMain.removeHandler(TEAM_UPDATE_MEMBER_ROLE);
  ipcMain.removeHandler(TEAM_GET_PROJECT_BRANCH);
  ipcMain.removeHandler(TEAM_GET_ATTACHMENTS);
  ipcMain.removeHandler(TEAM_KILL_PROCESS);
  ipcMain.removeHandler(TEAM_LEAD_ACTIVITY);
  ipcMain.removeHandler(TEAM_LEAD_CONTEXT);
  ipcMain.removeHandler(TEAM_MEMBER_SPAWN_STATUSES);
  ipcMain.removeHandler(TEAM_SOFT_DELETE_TASK);
  ipcMain.removeHandler(TEAM_RESTORE_TASK);
  ipcMain.removeHandler(TEAM_GET_DELETED_TASKS);
  ipcMain.removeHandler(TEAM_SET_TASK_CLARIFICATION);
  ipcMain.removeHandler(TEAM_SHOW_MESSAGE_NOTIFICATION);
  ipcMain.removeHandler(TEAM_ADD_TASK_RELATIONSHIP);
  ipcMain.removeHandler(TEAM_REMOVE_TASK_RELATIONSHIP);
  ipcMain.removeHandler(TEAM_SAVE_TASK_ATTACHMENT);
  ipcMain.removeHandler(TEAM_GET_TASK_ATTACHMENT);
  ipcMain.removeHandler(TEAM_DELETE_TASK_ATTACHMENT);
  ipcMain.removeHandler(TEAM_TOOL_APPROVAL_RESPOND);
  ipcMain.removeHandler(TEAM_VALIDATE_CLI_ARGS);
  ipcMain.removeHandler(TEAM_TOOL_APPROVAL_SETTINGS);
}

function getTeamDataService(): TeamDataService {
  if (!teamDataService) {
    throw new Error('Team handlers are not initialized');
  }
  return teamDataService;
}

function getTeamProvisioningService(): TeamProvisioningService {
  if (!teamProvisioningService) {
    throw new Error('Team provisioning handlers are not initialized');
  }
  return teamProvisioningService;
}

async function wrapTeamHandler<T>(
  operation: string,
  handler: () => Promise<T>
): Promise<IpcResult<T>> {
  try {
    const data = await handler();
    return { success: true, data };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error(`[teams:${operation}] ${message}`);
    return { success: false, error: message };
  }
}

async function handleGetProjectBranch(
  _event: IpcMainInvokeEvent,
  projectPath: unknown
): Promise<IpcResult<string | null>> {
  if (typeof projectPath !== 'string' || projectPath.trim().length === 0) {
    return { success: false, error: 'projectPath must be a non-empty string' };
  }
  try {
    const branch = await gitIdentityResolver.getBranch(path.normalize(projectPath.trim()));
    return { success: true, data: branch };
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error(`[teams:getProjectBranch] ${message}`);
    return { success: false, error: message };
  }
}

async function handleListTeams(_event: IpcMainInvokeEvent): Promise<IpcResult<TeamSummary[]>> {
  setCurrentMainOp('team:list');
  const startedAt = Date.now();
  try {
    return await wrapTeamHandler('list', () => getTeamDataService().listTeams());
  } finally {
    const ms = Date.now() - startedAt;
    if (ms >= 1500) {
      logger.warn(`[teams:list] slow ms=${ms}`);
    }
    setCurrentMainOp(null);
  }
}

async function handleGetData(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<TeamData>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  const tn = validated.value!;
  const startedAt = Date.now();
  let data: TeamData;
  try {
    data = await getTeamDataService().getTeamData(tn);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    if (
      message === `Team not found: ${tn}` &&
      getTeamProvisioningService().hasProvisioningRun(tn)
    ) {
      return { success: false, error: 'TEAM_PROVISIONING' };
    }
    logger.error(`[teams:getData] ${message}`);
    return { success: false, error: message };
  }
  const getDataMs = Date.now() - startedAt;
  if (getDataMs >= 1500) {
    logger.warn(`[teams:getData] slow team=${tn} ms=${getDataMs}`);
  }
  const provisioning = getTeamProvisioningService();
  const isAlive = provisioning.isTeamAlive(tn);

  const displayName = data.config.name || tn;
  const projectPath = data.config.projectPath;

  const live = provisioning.getLiveLeadProcessMessages(tn);
  if (live.length === 0) {
    checkRateLimitMessages(data.messages, tn, displayName, projectPath);
    return { success: true, data: { ...data, isAlive } };
  }

  const normalizeText = (text: string): string => text.trim().replace(/\r\n/g, '\n');
  const isLeadThoughtLike = (msg: { source?: unknown; to?: string }): boolean =>
    !msg.to && (msg.source === 'lead_process' || msg.source === 'lead_session');
  const getLeadThoughtFingerprint = (msg: {
    from: string;
    text: string;
    leadSessionId?: string;
  }): string => `${msg.leadSessionId ?? ''}\0${msg.from}\0${normalizeText(msg.text)}`;

  // Collect fingerprints only for thought-like lead messages. Include leadSessionId so a
  // repeated thought in a new session does not get collapsed into an old session's history.
  const existingTextFingerprints = new Set<string>();
  for (const msg of data.messages) {
    if (typeof msg.from !== 'string' || typeof msg.text !== 'string') continue;
    if (!isLeadThoughtLike(msg)) continue;
    existingTextFingerprints.add(getLeadThoughtFingerprint(msg));
  }

  const keyFor = (m: {
    messageId?: string;
    timestamp: string;
    from: string;
    text: string;
  }): string => {
    if (typeof m.messageId === 'string' && m.messageId.trim().length > 0) {
      return m.messageId;
    }
    return `${m.timestamp}\0${m.from}\0${(m.text ?? '').slice(0, 80)}`;
  };

  // Text-based fingerprints for live lead thoughts to catch duplicates with different
  // messageIds inside the same session (e.g. lead-turn-* re-emits).
  const leadProcessTextFingerprints = new Set<string>();

  const merged: typeof data.messages = [];
  const seen = new Set<string>();
  for (const msg of [...data.messages, ...live]) {
    if ((msg as { source?: unknown }).source === 'lead_process' && !msg.to) {
      const fp = getLeadThoughtFingerprint(msg);
      // Skip if the same thought already exists in persisted history for the same session.
      if (existingTextFingerprints.has(fp)) {
        continue;
      }
      // Dedup live lead_process thoughts with the same text in the same session.
      if (leadProcessTextFingerprints.has(fp)) {
        continue;
      }
      leadProcessTextFingerprints.add(fp);
    }
    const key = keyFor(msg);
    if (seen.has(key)) continue;
    seen.add(key);
    merged.push(msg);
  }
  merged.sort((a, b) => Date.parse(b.timestamp) - Date.parse(a.timestamp));

  checkRateLimitMessages(merged, tn, displayName, projectPath);
  return { success: true, data: { ...data, isAlive, messages: merged } };
}

async function handleDeleteTeam(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<void>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  return wrapTeamHandler('deleteTeam', async () => {
    getTeamProvisioningService().stopTeam(validated.value!);
    await getTeamDataService().deleteTeam(validated.value!);
  });
}

async function handleRestoreTeam(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<void>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  return wrapTeamHandler('restoreTeam', () => getTeamDataService().restoreTeam(validated.value!));
}

async function handlePermanentlyDeleteTeam(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<void>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  return wrapTeamHandler('permanentlyDeleteTeam', () =>
    getTeamDataService().permanentlyDeleteTeam(validated.value!)
  );
}

async function handleUpdateConfig(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  updates: unknown
): Promise<IpcResult<TeamConfig>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  if (!updates || typeof updates !== 'object') {
    return { success: false, error: 'Invalid updates object' };
  }
  const { name, description, color } = updates as TeamUpdateConfigRequest;
  if (name !== undefined && typeof name !== 'string') {
    return { success: false, error: 'name must be a string' };
  }
  if (description !== undefined && typeof description !== 'string') {
    return { success: false, error: 'description must be a string' };
  }
  if (color !== undefined && typeof color !== 'string') {
    return { success: false, error: 'color must be a string' };
  }
  return wrapTeamHandler('updateConfig', async () => {
    const tn = validated.value!;
    const result = await getTeamDataService().updateConfig(tn, {
      name,
      description,
      color,
    });
    if (!result) {
      throw new Error('Team config not found');
    }

    // Notify running lead about the rename so it stays aware of current team name
    if (typeof name === 'string' && name.trim()) {
      const provisioning = getTeamProvisioningService();
      if (provisioning.isTeamAlive(tn)) {
        const msg = `The team has been renamed to "${name.trim()}". Please use this name when referring to the team going forward.`;
        try {
          await provisioning.sendMessageToTeam(tn, msg);
        } catch {
          logger.warn(`Failed to notify lead about team rename for ${tn}`);
        }
      }
    }

    return result;
  });
}

function isProvisioningTeamName(teamName: string): boolean {
  if (teamName.length > 64) return false;
  const parts = teamName.split('-');
  return parts.every((p) => /^[a-z0-9]+$/.test(p));
}

const VALID_EFFORT_LEVELS: readonly string[] = ['low', 'medium', 'high'];

function isValidEffort(value: unknown): value is EffortLevel {
  return typeof value === 'string' && VALID_EFFORT_LEVELS.includes(value);
}

async function validateProvisioningRequest(
  request: unknown
): Promise<{ valid: true; value: TeamCreateRequest } | { valid: false; error: string }> {
  if (!request || typeof request !== 'object') {
    return { valid: false, error: 'Invalid team create request' };
  }

  const payload = request as Partial<TeamCreateRequest>;
  if (typeof payload.teamName !== 'string' || payload.teamName.trim().length === 0) {
    return { valid: false, error: 'teamName is required' };
  }
  const teamName = payload.teamName.trim();
  if (!isProvisioningTeamName(teamName)) {
    return { valid: false, error: 'teamName must be kebab-case [a-z0-9-], max 64 chars' };
  }

  if (payload.displayName !== undefined && typeof payload.displayName !== 'string') {
    return { valid: false, error: 'displayName must be string' };
  }
  if (payload.description !== undefined && typeof payload.description !== 'string') {
    return { valid: false, error: 'description must be string' };
  }

  if (!Array.isArray(payload.members)) {
    return { valid: false, error: 'members must be an array' };
  }

  const seenNames = new Set<string>();
  const members: TeamCreateRequest['members'] = [];
  for (const member of payload.members) {
    if (!member || typeof member !== 'object') {
      return { valid: false, error: 'member must be object' };
    }
    const nameValidation = validateTeammateName((member as { name?: unknown }).name);
    if (!nameValidation.valid) {
      return { valid: false, error: nameValidation.error ?? 'Invalid member name' };
    }
    const memberName = nameValidation.value!;
    if (seenNames.has(memberName)) {
      return { valid: false, error: 'member names must be unique' };
    }
    seenNames.add(memberName);

    const role = (member as { role?: unknown }).role;
    if (role !== undefined && typeof role !== 'string') {
      return { valid: false, error: 'member role must be string' };
    }
    const workflow = (member as { workflow?: unknown }).workflow;
    if (workflow !== undefined && typeof workflow !== 'string') {
      return { valid: false, error: 'member workflow must be string' };
    }
    members.push({
      name: memberName,
      role: typeof role === 'string' ? role.trim() : undefined,
      workflow: typeof workflow === 'string' ? workflow.trim() : undefined,
    });
  }

  if (typeof payload.cwd !== 'string' || payload.cwd.trim().length === 0) {
    return { valid: false, error: 'cwd is required' };
  }
  const cwd = payload.cwd.trim();
  if (!path.isAbsolute(cwd)) {
    return { valid: false, error: 'cwd must be an absolute path' };
  }

  if (payload.prompt !== undefined && typeof payload.prompt !== 'string') {
    return { valid: false, error: 'prompt must be a string' };
  }

  try {
    await fs.promises.mkdir(cwd, { recursive: true });
  } catch {
    return { valid: false, error: 'failed to create cwd directory' };
  }

  let stat: fs.Stats;
  try {
    stat = await fs.promises.stat(cwd);
  } catch {
    return { valid: false, error: 'cwd does not exist' };
  }
  if (!stat.isDirectory()) {
    return { valid: false, error: 'cwd must be a directory' };
  }

  if (payload.worktree !== undefined) {
    if (typeof payload.worktree !== 'string') {
      return { valid: false, error: 'worktree must be a string' };
    }
    const wt = payload.worktree.trim();
    if (wt.length > 128) {
      return { valid: false, error: 'worktree name too long (max 128)' };
    }
    if (wt && !/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(wt)) {
      return {
        valid: false,
        error: 'worktree name: start with alphanumeric, use [a-zA-Z0-9._-]',
      };
    }
  }
  if (payload.extraCliArgs !== undefined) {
    if (typeof payload.extraCliArgs !== 'string') {
      return { valid: false, error: 'extraCliArgs must be a string' };
    }
    if (payload.extraCliArgs.length > 1024) {
      return { valid: false, error: 'extraCliArgs too long (max 1024)' };
    }
  }

  return {
    valid: true,
    value: {
      teamName,
      displayName: payload.displayName?.trim() || undefined,
      description: payload.description?.trim() || undefined,
      color: typeof payload.color === 'string' ? payload.color.trim() || undefined : undefined,
      members,
      cwd,
      prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
      model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
      effort: isValidEffort(payload.effort) ? payload.effort : undefined,
      skipPermissions:
        typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
      worktree:
        typeof payload.worktree === 'string' && payload.worktree.trim()
          ? payload.worktree.trim()
          : undefined,
      extraCliArgs:
        typeof payload.extraCliArgs === 'string' && payload.extraCliArgs.trim()
          ? payload.extraCliArgs.trim()
          : undefined,
    },
  };
}

async function handleGetClaudeLogs(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  query?: unknown
): Promise<IpcResult<TeamClaudeLogsResponse>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }

  let parsed: TeamClaudeLogsQuery | undefined;
  if (query !== undefined) {
    if (!query || typeof query !== 'object') {
      return { success: false, error: 'query must be an object' };
    }
    const q = query as Record<string, unknown>;
    parsed = {
      offset: typeof q.offset === 'number' ? q.offset : undefined,
      limit: typeof q.limit === 'number' ? q.limit : undefined,
    };
  }

  return wrapTeamHandler('getClaudeLogs', async () => {
    const data = getTeamProvisioningService().getClaudeLogs(validated.value!, parsed);
    return {
      lines: data.lines,
      total: data.total,
      hasMore: data.hasMore,
      updatedAt: data.updatedAt,
    };
  });
}

async function handleCreateTeam(
  event: IpcMainInvokeEvent,
  request: unknown
): Promise<IpcResult<TeamCreateResponse>> {
  const validation = await validateProvisioningRequest(request);
  if (!validation.valid) {
    return { success: false, error: validation.error };
  }

  return wrapTeamHandler('create', () =>
    getTeamProvisioningService().createTeam(validation.value, (progress) => {
      try {
        event.sender.send(TEAM_PROVISIONING_PROGRESS, progress);
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        logger.warn(`Failed to emit provisioning progress: ${message}`);
      }
    })
  );
}

async function handleLaunchTeam(
  event: IpcMainInvokeEvent,
  request: unknown
): Promise<IpcResult<TeamLaunchResponse>> {
  if (!request || typeof request !== 'object') {
    return { success: false, error: 'Invalid team launch request' };
  }

  const payload = request as Partial<TeamLaunchRequest>;
  const validatedTeamName = validateTeamName(payload.teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  if (typeof payload.cwd !== 'string' || payload.cwd.trim().length === 0) {
    return { success: false, error: 'cwd is required' };
  }
  const cwd = payload.cwd.trim();
  if (!path.isAbsolute(cwd)) {
    return { success: false, error: 'cwd must be an absolute path' };
  }

  try {
    const stat = await fs.promises.stat(cwd);
    if (!stat.isDirectory()) {
      return { success: false, error: 'cwd must be a directory' };
    }
  } catch {
    return { success: false, error: 'cwd does not exist' };
  }

  if (payload.prompt !== undefined && typeof payload.prompt !== 'string') {
    return { success: false, error: 'prompt must be a string' };
  }

  if (payload.model !== undefined && typeof payload.model !== 'string') {
    return { success: false, error: 'model must be a string' };
  }

  return wrapTeamHandler('launch', () =>
    getTeamProvisioningService().launchTeam(
      {
        teamName: validatedTeamName.value!,
        cwd,
        prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
        model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
        effort: isValidEffort(payload.effort) ? payload.effort : undefined,
        clearContext: payload.clearContext === true ? true : undefined,
        skipPermissions:
          typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
        worktree:
          typeof payload.worktree === 'string' ? payload.worktree.trim() || undefined : undefined,
        extraCliArgs:
          typeof payload.extraCliArgs === 'string'
            ? payload.extraCliArgs.trim() || undefined
            : undefined,
      },
      (progress) => {
        try {
          event.sender.send(TEAM_PROVISIONING_PROGRESS, progress);
        } catch (error) {
          const message = error instanceof Error ? error.message : String(error);
          logger.warn(`Failed to emit launch provisioning progress: ${message}`);
        }
      }
    )
  );
}

async function handleValidateCliArgs(
  _event: IpcMainInvokeEvent,
  rawArgs: unknown
): Promise<IpcResult<CliArgsValidationResult>> {
  if (typeof rawArgs !== 'string') {
    return { success: false, error: 'rawArgs must be a string' };
  }
  if (rawArgs.length > 2048) {
    return { success: false, error: 'rawArgs too long (max 2048)' };
  }
  return wrapTeamHandler('validateCliArgs', async () => {
    const helpOutput = await getTeamProvisioningService().getCliHelpOutput();
    const knownFlags = extractFlagsFromHelp(helpOutput);
    const userFlags = extractUserFlags(rawArgs);

    const invalidFlags = userFlags.filter((f) => !knownFlags.has(f));
    const protectedFlags = userFlags.filter((f) => PROTECTED_CLI_FLAGS.has(f));
    const allBad = [...new Set([...invalidFlags, ...protectedFlags])];

    return {
      valid: allBad.length === 0,
      invalidFlags: allBad.length > 0 ? allBad : undefined,
    };
  });
}

async function handlePrepareProvisioning(
  _event: IpcMainInvokeEvent,
  cwd: unknown
): Promise<IpcResult<TeamProvisioningPrepareResult>> {
  let validatedCwd: string | undefined;
  if (cwd !== undefined) {
    if (typeof cwd !== 'string' || cwd.trim().length === 0) {
      return { success: false, error: 'cwd must be a non-empty string' };
    }
    validatedCwd = cwd.trim();
    if (!path.isAbsolute(validatedCwd)) {
      return { success: false, error: 'cwd must be an absolute path' };
    }
  }
  return wrapTeamHandler('prepareProvisioning', () =>
    getTeamProvisioningService().prepareForProvisioning(validatedCwd)
  );
}

async function handleProvisioningStatus(
  _event: IpcMainInvokeEvent,
  runId: unknown
): Promise<IpcResult<TeamProvisioningProgress>> {
  if (typeof runId !== 'string' || runId.trim().length === 0) {
    return { success: false, error: 'runId is required' };
  }
  return wrapTeamHandler('provisioningStatus', () =>
    getTeamProvisioningService().getProvisioningStatus(runId.trim())
  );
}

async function handleCancelProvisioning(
  _event: IpcMainInvokeEvent,
  runId: unknown
): Promise<IpcResult<void>> {
  if (typeof runId !== 'string' || runId.trim().length === 0) {
    return { success: false, error: 'runId is required' };
  }
  return wrapTeamHandler('cancelProvisioning', () =>
    getTeamProvisioningService().cancelProvisioning(runId.trim())
  );
}

function isUpdateKanbanPatch(value: unknown): value is UpdateKanbanPatch {
  if (!value || typeof value !== 'object') {
    return false;
  }

  const patch = value as Partial<UpdateKanbanPatch> & { op?: unknown; column?: unknown };
  if (patch.op === 'remove') {
    return true;
  }

  if (patch.op === 'request_changes') {
    return patch.comment === undefined || typeof patch.comment === 'string';
  }

  return patch.op === 'set_column' && (patch.column === 'review' || patch.column === 'approved');
}

async function handleGetAttachments(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  messageId: unknown
): Promise<IpcResult<AttachmentFileData[]>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  if (typeof messageId !== 'string' || messageId.trim().length === 0) {
    return { success: false, error: 'messageId must be a non-empty string' };
  }
  const safeMessageId = messageId.trim();
  if (safeMessageId.includes('/') || safeMessageId.includes('\\') || safeMessageId.includes('..')) {
    return { success: false, error: 'Invalid messageId' };
  }
  return wrapTeamHandler('getAttachments', () =>
    attachmentStore.getAttachments(vTeam.value!, safeMessageId)
  );
}

function validateAttachments(
  attachments: unknown
): { valid: true; value: AttachmentPayload[] } | { valid: false; error: string } {
  if (!Array.isArray(attachments)) {
    return { valid: false, error: 'attachments must be an array' };
  }
  if (attachments.length > MAX_ATTACHMENTS) {
    return { valid: false, error: `Maximum ${MAX_ATTACHMENTS} attachments allowed` };
  }
  let totalSize = 0;
  const result: AttachmentPayload[] = [];
  for (const att of attachments) {
    if (!att || typeof att !== 'object') {
      return { valid: false, error: 'Invalid attachment entry' };
    }
    const a = att as Partial<AttachmentPayload>;
    if (typeof a.id !== 'string' || typeof a.filename !== 'string') {
      return { valid: false, error: 'Attachment must have id and filename' };
    }
    if (typeof a.data !== 'string' || typeof a.mimeType !== 'string') {
      return { valid: false, error: 'Attachment must have data and mimeType' };
    }
    if (typeof a.size !== 'number' || a.size <= 0) {
      return { valid: false, error: 'Attachment must have a positive size' };
    }
    if (!ALLOWED_ATTACHMENT_TYPES.has(a.mimeType)) {
      return { valid: false, error: `Unsupported attachment type: ${a.mimeType}` };
    }
    if (a.size > MAX_ATTACHMENT_SIZE) {
      return { valid: false, error: `Attachment "${a.filename}" exceeds 10MB limit` };
    }
    // Sanity check: base64 data should be roughly 4/3 of the reported binary size
    const estimatedBinarySize = Math.ceil(a.data.length * 0.75);
    if (estimatedBinarySize > MAX_ATTACHMENT_SIZE * 1.1) {
      return { valid: false, error: `Attachment "${a.filename}" data exceeds size limit` };
    }
    totalSize += Math.max(a.size, estimatedBinarySize);
    result.push({
      id: a.id,
      filename: a.filename,
      data: a.data,
      mimeType: a.mimeType,
      size: a.size,
    });
  }
  if (totalSize > MAX_TOTAL_ATTACHMENT_SIZE) {
    return { valid: false, error: 'Total attachment size exceeds 20MB limit' };
  }
  return { valid: true, value: result };
}

function buildMessageDeliveryText(
  baseText: string,
  opts: {
    actionMode?: AgentActionMode;
    isLeadRecipient: boolean;
  }
): string {
  const hiddenBlocks: string[] = [];
  const actionModeBlock = buildActionModeAgentBlock(opts.actionMode);
  if (actionModeBlock) {
    hiddenBlocks.push(actionModeBlock);
  }
  if (!opts.isLeadRecipient) {
    hiddenBlocks.push(
      [
        AGENT_BLOCK_OPEN,
        'You received a direct message from the human user via the UI.',
        'Please reply back to recipient "user" with a short, human-readable answer.',
        'If you cannot respond now, reply with a brief status (e.g. "Busy, will reply later").',
        AGENT_BLOCK_CLOSE,
      ].join('\n')
    );
  }

  if (hiddenBlocks.length === 0) {
    return baseText;
  }

  return [...hiddenBlocks, baseText].join('\n\n');
}

async function handleSendMessage(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  request: unknown
): Promise<IpcResult<SendMessageResult>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  if (!request || typeof request !== 'object') {
    return { success: false, error: 'Invalid send message request' };
  }

  const payload = request as Partial<SendMessageRequest>;
  const validatedMember = validateMemberName(payload.member);
  if (!validatedMember.valid) {
    return { success: false, error: validatedMember.error ?? 'Invalid member' };
  }
  if (typeof payload.text !== 'string' || payload.text.trim().length === 0) {
    return { success: false, error: 'text must be non-empty string' };
  }
  if (payload.summary !== undefined && typeof payload.summary !== 'string') {
    return { success: false, error: 'summary must be string' };
  }
  if (payload.from !== undefined) {
    const validatedFrom = validateFromField(payload.from);
    if (!validatedFrom.valid) {
      return { success: false, error: validatedFrom.error ?? 'Invalid from' };
    }
  }
  if (payload.actionMode !== undefined && !isAgentActionMode(payload.actionMode)) {
    return { success: false, error: 'actionMode must be one of: do, ask, delegate' };
  }

  let validatedAttachments: AttachmentPayload[] | undefined;
  if (
    payload.attachments !== undefined &&
    Array.isArray(payload.attachments) &&
    payload.attachments.length > 0
  ) {
    const attResult = validateAttachments(payload.attachments);
    if (!attResult.valid) {
      return { success: false, error: attResult.error };
    }
    validatedAttachments = attResult.value;
  }

  const tn = validatedTeamName.value!;
  const memberName = validatedMember.value!;
  let prevalidatedLeadName: string | null | undefined;
  let prevalidatedIsLeadRecipient: boolean | undefined;
  if (payload.actionMode === 'delegate') {
    try {
      prevalidatedLeadName = await getTeamDataService().getLeadMemberName(tn);
    } catch (error) {
      return wrapTeamHandler('sendMessage', async () => {
        throw error;
      });
    }
    prevalidatedIsLeadRecipient =
      prevalidatedLeadName !== null && memberName === prevalidatedLeadName;
    if (!prevalidatedIsLeadRecipient) {
      return {
        success: false,
        error: 'Delegate mode is only supported when messaging the team lead',
      };
    }
  }

  return wrapTeamHandler('sendMessage', async () => {
    const provisioning = getTeamProvisioningService();
    const isAlive = provisioning.isTeamAlive(tn);

    const leadName =
      prevalidatedLeadName !== undefined
        ? prevalidatedLeadName
        : await getTeamDataService().getLeadMemberName(tn);
    const isLeadRecipient =
      prevalidatedIsLeadRecipient !== undefined
        ? prevalidatedIsLeadRecipient
        : leadName !== null && memberName === leadName;
    const actionMode = payload.actionMode;

    // Attachments only supported for live lead (stdin content blocks)
    if (validatedAttachments?.length && (!isLeadRecipient || !isAlive)) {
      throw new Error(
        'Attachments are only supported when sending to the team lead while the team is online'
      );
    }

    // Smart routing: lead + alive → stdin direct, else → inbox
    if (isLeadRecipient && isAlive) {
      const resolvedLeadName = leadName ?? memberName;
      // Separate try blocks: stdin delivery vs persistence
      // If stdin succeeds but persistence fails, do NOT fallback to inbox (would duplicate)
      // Wrap with instructions so lead responds with visible text (not just agent-only blocks)
      const wrappedText = [
        `You received a direct message from the user.`,
        `IMPORTANT: Your text response here is shown to the user in the Messages panel. Always include a brief human-readable reply. Do NOT respond with only an agent-only block.`,
        ``,
        `Message from user:`,
        buildMessageDeliveryText(payload.text!, {
          actionMode,
          isLeadRecipient: true,
        }),
      ].join('\n');

      let stdinSent = false;
      try {
        await provisioning.sendMessageToTeam(tn, wrappedText, validatedAttachments);
        stdinSent = true;
      } catch (stdinError: unknown) {
        // Stdin failed (process died between check and write)
        // If attachments were requested, fail rather than silently dropping them
        if (validatedAttachments?.length) {
          throw new Error(
            'Failed to deliver message with attachments: team process became unavailable'
          );
        }
        const errMsg = stdinError instanceof Error ? stdinError.message : 'unknown error';
        logger.warn(`stdin fallback for ${tn}: ${errMsg}`);
        // Fallback to inbox path below
      }

      if (stdinSent) {
        const attachmentMeta: AttachmentMeta[] | undefined = validatedAttachments?.map((a) => ({
          id: a.id,
          filename: a.filename,
          mimeType: a.mimeType,
          size: a.size,
        }));

        // Persistence is best-effort — stdin already delivered the message
        let result: SendMessageResult;
        try {
          result = await getTeamDataService().sendDirectToLead(
            tn,
            resolvedLeadName,
            payload.text!,
            payload.summary,
            attachmentMeta
          );
        } catch (persistError) {
          logger.warn(`Persistence failed after stdin delivery for ${tn}: ${String(persistError)}`);
          result = { deliveredToInbox: false, messageId: `stdin-${Date.now()}` };
        }

        // Save attachment binary data to disk (best-effort)
        if (validatedAttachments?.length && result.messageId) {
          void attachmentStore
            .saveAttachments(tn, result.messageId, validatedAttachments)
            .catch((e) => logger.warn(`Failed to save attachments: ${e}`));
        }

        provisioning.pushLiveLeadProcessMessage(tn, {
          from: 'user',
          to: resolvedLeadName,
          text: payload.text!,
          timestamp: new Date().toISOString(),
          read: true,
          summary: payload.summary,
          messageId: result.messageId,
          source: 'user_sent',
          attachments: attachmentMeta,
        });

        return result;
      }
    }

    // Inbox path: offline lead or regular members (no attachment support)
    const baseText = payload.text!.trim();
    const memberDeliveryText = buildMessageDeliveryText(baseText, {
      actionMode,
      isLeadRecipient,
    });
    const result = await getTeamDataService().sendMessage(tn, {
      member: memberName,
      text: memberDeliveryText,
      summary: payload.summary,
      from: payload.from,
      source: 'user_sent',
    });

    // Best-effort live relay so active processes see the inbox row promptly.
    if (!isLeadRecipient && isAlive) {
      try {
        await provisioning.relayMemberInboxMessages(tn, memberName);
      } catch (e: unknown) {
        logger.warn(`Relay after sendMessage failed for teammate "${memberName}": ${String(e)}`);
      }
    }

    // Best-effort relay for lead via inbox
    if (isLeadRecipient && isAlive) {
      void provisioning
        .relayLeadInboxMessages(tn)
        .catch((e: unknown) =>
          logger.warn(`Relay after sendMessage failed for ${tn}: ${String(e)}`)
        );
    }

    return result;
  });
}

async function handleCreateTask(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  request: unknown
): Promise<IpcResult<TeamTask>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  if (!request || typeof request !== 'object') {
    return { success: false, error: 'Invalid create task request' };
  }

  const payload = request as Partial<CreateTaskRequest>;
  if (typeof payload.subject !== 'string' || payload.subject.trim().length === 0) {
    return { success: false, error: 'subject must be a non-empty string' };
  }
  if (payload.subject.trim().length > 500) {
    return { success: false, error: 'subject exceeds max length (500)' };
  }
  if (payload.description !== undefined && typeof payload.description !== 'string') {
    return { success: false, error: 'description must be string' };
  }
  if (payload.owner !== undefined) {
    const validatedOwner = validateMemberName(payload.owner);
    if (!validatedOwner.valid) {
      return { success: false, error: validatedOwner.error ?? 'Invalid owner' };
    }
  }
  if (payload.blockedBy !== undefined) {
    if (
      !Array.isArray(payload.blockedBy) ||
      payload.blockedBy.some((id) => typeof id !== 'string')
    ) {
      return { success: false, error: 'blockedBy must be an array of task ID strings' };
    }
  }
  if (payload.related !== undefined) {
    if (!Array.isArray(payload.related) || payload.related.some((id) => typeof id !== 'string')) {
      return { success: false, error: 'related must be an array of task ID strings' };
    }
    for (const id of payload.related) {
      const validated = validateTaskId(id);
      if (!validated.valid) {
        return { success: false, error: validated.error ?? 'Invalid related task id' };
      }
    }
  }
  if (payload.prompt !== undefined) {
    if (typeof payload.prompt !== 'string') {
      return { success: false, error: 'prompt must be a string' };
    }
    if (payload.prompt.length > 5000) {
      return { success: false, error: 'prompt exceeds max length (5000)' };
    }
  }
  if (payload.startImmediately !== undefined && typeof payload.startImmediately !== 'boolean') {
    return { success: false, error: 'startImmediately must be a boolean' };
  }

  return wrapTeamHandler('createTask', () =>
    getTeamDataService().createTask(validatedTeamName.value!, {
      subject: payload.subject!.trim(),
      description: payload.description?.trim(),
      owner: payload.owner?.trim() || undefined,
      blockedBy: payload.blockedBy,
      related: payload.related,
      prompt: payload.prompt?.trim() || undefined,
      startImmediately: payload.startImmediately,
    })
  );
}

async function handleRequestReview(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  const validatedTaskId = validateTaskId(taskId);
  if (!validatedTaskId.valid) {
    return { success: false, error: validatedTaskId.error ?? 'Invalid taskId' };
  }

  return wrapTeamHandler('requestReview', () =>
    getTeamDataService().requestReview(validatedTeamName.value!, validatedTaskId.value!)
  );
}

async function handleUpdateKanban(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  patch: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  const validatedTaskId = validateTaskId(taskId);
  if (!validatedTaskId.valid) {
    return { success: false, error: validatedTaskId.error ?? 'Invalid taskId' };
  }

  if (!isUpdateKanbanPatch(patch)) {
    return { success: false, error: 'Invalid kanban patch' };
  }

  return wrapTeamHandler('updateKanban', async () => {
    await getTeamDataService().updateKanban(
      validatedTeamName.value!,
      validatedTaskId.value!,
      patch
    );
  });
}

function validateKanbanColumnId(
  value: unknown
): { valid: true; value: KanbanColumnId } | { valid: false; error: string } {
  if (typeof value !== 'string' || !KANBAN_COLUMN_IDS.includes(value as KanbanColumnId)) {
    return { valid: false, error: `columnId must be one of: ${KANBAN_COLUMN_IDS.join(', ')}` };
  }
  return { valid: true, value: value as KanbanColumnId };
}

async function handleUpdateKanbanColumnOrder(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  columnId: unknown,
  orderedTaskIds: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }
  const validatedColumnId = validateKanbanColumnId(columnId);
  if (!validatedColumnId.valid) {
    return { success: false, error: validatedColumnId.error ?? 'Invalid columnId' };
  }
  if (!Array.isArray(orderedTaskIds)) {
    return { success: false, error: 'orderedTaskIds must be an array' };
  }
  const ids = orderedTaskIds.filter((id): id is string => typeof id === 'string');
  return wrapTeamHandler('updateKanbanColumnOrder', () =>
    getTeamDataService().updateKanbanColumnOrder(
      validatedTeamName.value!,
      validatedColumnId.value,
      ids
    )
  );
}

const VALID_TASK_STATUSES: TeamTaskStatus[] = ['pending', 'in_progress', 'completed'];

async function handleUpdateTaskStatus(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  status: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  const validatedTaskId = validateTaskId(taskId);
  if (!validatedTaskId.valid) {
    return { success: false, error: validatedTaskId.error ?? 'Invalid taskId' };
  }

  if (typeof status !== 'string' || !VALID_TASK_STATUSES.includes(status as TeamTaskStatus)) {
    return { success: false, error: `status must be one of: ${VALID_TASK_STATUSES.join(', ')}` };
  }

  return wrapTeamHandler('updateTaskStatus', () =>
    getTeamDataService().updateTaskStatus(
      validatedTeamName.value!,
      validatedTaskId.value!,
      status as TeamTaskStatus
    )
  );
}

async function handleSoftDeleteTask(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  const validatedTaskId = validateTaskId(taskId);
  if (!validatedTaskId.valid) {
    return { success: false, error: validatedTaskId.error ?? 'Invalid taskId' };
  }

  return wrapTeamHandler('softDeleteTask', () =>
    getTeamDataService().softDeleteTask(validatedTeamName.value!, validatedTaskId.value!)
  );
}

async function handleRestoreTask(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  const validatedTaskId = validateTaskId(taskId);
  if (!validatedTaskId.valid) {
    return { success: false, error: validatedTaskId.error ?? 'Invalid taskId' };
  }

  return wrapTeamHandler('restoreTask', () =>
    getTeamDataService().restoreTask(validatedTeamName.value!, validatedTaskId.value!)
  );
}

async function handleGetDeletedTasks(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<TeamTask[]>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  return wrapTeamHandler('getDeletedTasks', () =>
    getTeamDataService().getDeletedTasks(validatedTeamName.value!)
  );
}

const VALID_CLARIFICATION_VALUES = ['lead', 'user'] as const;

async function handleSetTaskClarification(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  value: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  const validatedTaskId = validateTaskId(taskId);
  if (!validatedTaskId.valid) {
    return { success: false, error: validatedTaskId.error ?? 'Invalid taskId' };
  }

  if (
    value !== null &&
    (typeof value !== 'string' || !VALID_CLARIFICATION_VALUES.includes(value as 'lead' | 'user'))
  ) {
    return {
      success: false,
      error: `value must be "lead", "user", or null`,
    };
  }

  return wrapTeamHandler('setTaskClarification', () =>
    getTeamDataService().setTaskNeedsClarification(
      validatedTeamName.value!,
      validatedTaskId.value!,
      value as 'lead' | 'user' | null
    )
  );
}

async function handleUpdateTaskOwner(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  owner: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }

  const validatedTaskId = validateTaskId(taskId);
  if (!validatedTaskId.valid) {
    return { success: false, error: validatedTaskId.error ?? 'Invalid taskId' };
  }

  let nextOwner: string | null = null;
  if (owner !== null) {
    const validatedOwner = validateMemberName(owner);
    if (!validatedOwner.valid) {
      return { success: false, error: validatedOwner.error ?? 'Invalid owner' };
    }
    nextOwner = validatedOwner.value!;
  }

  return wrapTeamHandler('updateTaskOwner', () =>
    getTeamDataService().updateTaskOwner(
      validatedTeamName.value!,
      validatedTaskId.value!,
      nextOwner
    )
  );
}

async function handleProcessSend(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  message: unknown
): Promise<IpcResult<void>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }
  if (typeof message !== 'string' || message.trim().length === 0) {
    return { success: false, error: 'message must be a non-empty string' };
  }
  return wrapTeamHandler('processSend', () =>
    getTeamProvisioningService().sendMessageToTeam(validatedTeamName.value!, message)
  );
}

async function handleProcessAlive(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<boolean>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }
  return wrapTeamHandler('processAlive', async () =>
    getTeamProvisioningService().isTeamAlive(validatedTeamName.value!)
  );
}

async function handleCreateConfig(
  _event: IpcMainInvokeEvent,
  request: unknown
): Promise<IpcResult<void>> {
  if (!request || typeof request !== 'object') {
    return { success: false, error: 'Invalid create config request' };
  }

  const payload = request as Partial<TeamCreateConfigRequest>;
  if (typeof payload.teamName !== 'string' || payload.teamName.trim().length === 0) {
    return { success: false, error: 'teamName is required' };
  }
  const teamName = payload.teamName.trim();
  if (!isProvisioningTeamName(teamName)) {
    return { success: false, error: 'teamName must be kebab-case [a-z0-9-], max 64 chars' };
  }

  if (!Array.isArray(payload.members)) {
    return { success: false, error: 'members must be an array' };
  }

  if (payload.displayName !== undefined && typeof payload.displayName !== 'string') {
    return { success: false, error: 'displayName must be a string' };
  }
  if (payload.description !== undefined && typeof payload.description !== 'string') {
    return { success: false, error: 'description must be a string' };
  }
  if (payload.color !== undefined && typeof payload.color !== 'string') {
    return { success: false, error: 'color must be a string' };
  }
  if (payload.cwd !== undefined) {
    if (typeof payload.cwd !== 'string' || payload.cwd.trim().length === 0) {
      return { success: false, error: 'cwd must be a non-empty string if provided' };
    }
    if (!path.isAbsolute(payload.cwd.trim())) {
      return { success: false, error: 'cwd must be an absolute path' };
    }
  }

  const seenNames = new Set<string>();
  const members: TeamCreateConfigRequest['members'] = [];
  for (const member of payload.members) {
    if (!member || typeof member !== 'object') {
      return { success: false, error: 'member must be object' };
    }
    const nameValidation = validateTeammateName((member as { name?: unknown }).name);
    if (!nameValidation.valid) {
      return { success: false, error: nameValidation.error ?? 'Invalid member name' };
    }
    const memberName = nameValidation.value!;
    if (seenNames.has(memberName)) {
      return { success: false, error: 'member names must be unique' };
    }
    seenNames.add(memberName);

    const role = (member as { role?: unknown }).role;
    if (role !== undefined && typeof role !== 'string') {
      return { success: false, error: 'member role must be string' };
    }
    const workflow = (member as { workflow?: unknown }).workflow;
    if (workflow !== undefined && typeof workflow !== 'string') {
      return { success: false, error: 'member workflow must be string' };
    }
    members.push({
      name: memberName,
      role: typeof role === 'string' ? role.trim() : undefined,
      workflow: typeof workflow === 'string' ? workflow.trim() : undefined,
    });
  }

  return wrapTeamHandler('createConfig', () =>
    getTeamDataService().createTeamConfig({
      teamName,
      displayName: payload.displayName?.trim() || undefined,
      description: payload.description?.trim() || undefined,
      color: typeof payload.color === 'string' ? payload.color.trim() || undefined : undefined,
      members,
      cwd: typeof payload.cwd === 'string' ? payload.cwd.trim() || undefined : undefined,
    })
  );
}

function getTeamMemberLogsFinder(): TeamMemberLogsFinder {
  if (!teamMemberLogsFinder) {
    throw new Error('Team member logs finder is not initialized');
  }
  return teamMemberLogsFinder;
}

async function handleGetMemberLogs(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  memberName: unknown
): Promise<IpcResult<MemberLogSummary[]>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) {
    return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  }
  const vMember = validateMemberName(memberName);
  if (!vMember.valid) {
    return { success: false, error: vMember.error ?? 'Invalid memberName' };
  }
  return wrapTeamHandler('getMemberLogs', () =>
    getTeamMemberLogsFinder().findMemberLogs(vTeam.value!, vMember.value!)
  );
}

async function handleGetLogsForTask(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  options?: {
    owner?: string;
    status?: string;
    intervals?: { startedAt: string; completedAt?: string }[];
    since?: string;
  }
): Promise<IpcResult<MemberLogSummary[]>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) {
    return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  }
  const vTask = validateTaskId(taskId);
  if (!vTask.valid) {
    return { success: false, error: vTask.error ?? 'Invalid taskId' };
  }
  const opts =
    options && typeof options === 'object'
      ? {
          owner: typeof options.owner === 'string' ? options.owner : undefined,
          status: typeof options.status === 'string' ? options.status : undefined,
          since: typeof options.since === 'string' ? options.since : undefined,
          intervals: Array.isArray(options.intervals)
            ? (options.intervals as unknown[]).filter(
                (i): i is { startedAt: string; completedAt?: string } =>
                  Boolean(i) &&
                  typeof i === 'object' &&
                  typeof (i as Record<string, unknown>).startedAt === 'string' &&
                  ((i as Record<string, unknown>).completedAt === undefined ||
                    typeof (i as Record<string, unknown>).completedAt === 'string')
              )
            : undefined,
        }
      : undefined;
  return wrapTeamHandler('getLogsForTask', () =>
    getTeamMemberLogsFinder().findLogsForTask(vTeam.value!, vTask.value!, opts)
  );
}

function getMemberStatsComputer(): MemberStatsComputer {
  if (!memberStatsComputer) {
    throw new Error('Member stats computer is not initialized');
  }
  return memberStatsComputer;
}

async function handleGetMemberStats(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  memberName: unknown
): Promise<IpcResult<MemberFullStats>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) {
    return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  }
  const vMember = validateMemberName(memberName);
  if (!vMember.valid) {
    return { success: false, error: vMember.error ?? 'Invalid memberName' };
  }
  return wrapTeamHandler('getMemberStats', () =>
    getMemberStatsComputer().getStats(vTeam.value!, vMember.value!)
  );
}

async function handleAliveList(_event: IpcMainInvokeEvent): Promise<IpcResult<string[]>> {
  return wrapTeamHandler('aliveList', async () => getTeamProvisioningService().getAliveTeams());
}

async function handleLeadActivity(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<string>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  return wrapTeamHandler('leadActivity', async () =>
    getTeamProvisioningService().getLeadActivityState(validated.value!)
  );
}

async function handleLeadContext(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<LeadContextUsage | null>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  return wrapTeamHandler('leadContext', async () =>
    getTeamProvisioningService().getLeadContextUsage(validated.value!)
  );
}

async function handleMemberSpawnStatuses(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<Record<string, MemberSpawnStatusEntry>>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  return wrapTeamHandler('memberSpawnStatuses', async () =>
    getTeamProvisioningService().getMemberSpawnStatuses(validated.value!)
  );
}

async function handleStopTeam(
  _event: IpcMainInvokeEvent,
  teamName: unknown
): Promise<IpcResult<void>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  return wrapTeamHandler('stop', async () => {
    getTeamProvisioningService().stopTeam(validated.value!);
  });
}

async function handleStartTask(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown
): Promise<IpcResult<{ notifiedOwner: boolean }>> {
  const validatedTeamName = validateTeamName(teamName);
  if (!validatedTeamName.valid) {
    return { success: false, error: validatedTeamName.error ?? 'Invalid teamName' };
  }
  const validatedTaskId = validateTaskId(taskId);
  if (!validatedTaskId.valid) {
    return { success: false, error: validatedTaskId.error ?? 'Invalid taskId' };
  }
  return wrapTeamHandler('startTask', () =>
    getTeamDataService().startTask(validatedTeamName.value!, validatedTaskId.value!)
  );
}

async function handleGetAllTasks(_event: IpcMainInvokeEvent): Promise<IpcResult<GlobalTask[]>> {
  setCurrentMainOp('team:getAllTasks');
  const startedAt = Date.now();
  try {
    return await wrapTeamHandler('getAllTasks', () => getTeamDataService().getAllTasks());
  } finally {
    const ms = Date.now() - startedAt;
    if (ms >= 1500) {
      logger.warn(`[teams:getAllTasks] slow ms=${ms}`);
    }
    setCurrentMainOp(null);
  }
}

async function handleAddMember(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  payload: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };

  if (!payload || typeof payload !== 'object') {
    return { success: false, error: 'Invalid payload' };
  }
  const { name, role, workflow } = payload as {
    name?: unknown;
    role?: unknown;
    workflow?: unknown;
  };
  const vName = validateTeammateName(name);
  if (!vName.valid) return { success: false, error: vName.error ?? 'Invalid member name' };
  if (role !== undefined && typeof role !== 'string') {
    return { success: false, error: 'role must be a string' };
  }
  if (workflow !== undefined && typeof workflow !== 'string') {
    return { success: false, error: 'workflow must be a string' };
  }

  return wrapTeamHandler('addMember', async () => {
    const tn = vTeam.value!;
    const memberName = vName.value!;
    await getTeamDataService().addMember(tn, {
      name: memberName,
      role: role,
      workflow: typeof workflow === 'string' ? workflow.trim() || undefined : undefined,
    });

    // If team is alive, notify the lead to spawn the new teammate
    const provisioning = getTeamProvisioningService();
    if (provisioning.isTeamAlive(tn)) {
      const roleHint = typeof role === 'string' && role.trim() ? ` with role "${role.trim()}"` : '';
      const workflowHint =
        typeof workflow === 'string' && workflow.trim()
          ? ` Their workflow: ${workflow.trim()}`
          : '';
      const spawnMessage =
        `A new teammate "${memberName}"${roleHint} has been added to the team. ` +
        `Please spawn them immediately using the Task tool with team_name="${tn}" and name="${memberName}".${workflowHint}`;
      try {
        await provisioning.sendMessageToTeam(tn, spawnMessage);
      } catch {
        // Best-effort: lead process may not be responsive
        logger.warn(`Failed to notify lead about new member "${memberName}" in ${tn}`);
      }
    }
  });
}

async function handleReplaceMembers(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  request: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  if (!request || typeof request !== 'object') {
    return { success: false, error: 'request must be an object' };
  }
  const payload = request as { members?: unknown };
  if (!Array.isArray(payload.members)) {
    return { success: false, error: 'members must be an array' };
  }
  const seenNames = new Set<string>();
  const members: { name: string; role?: string; workflow?: string }[] = [];
  for (const item of payload.members) {
    if (!item || typeof item !== 'object') {
      return { success: false, error: 'member must be object' };
    }
    const m = item as { name?: unknown; role?: unknown; workflow?: unknown };
    const vName = validateTeammateName(m.name);
    if (!vName.valid) return { success: false, error: vName.error ?? 'Invalid member name' };
    const name = vName.value!;
    if (seenNames.has(name)) return { success: false, error: 'member names must be unique' };
    seenNames.add(name);
    if (m.role !== undefined && typeof m.role !== 'string') {
      return { success: false, error: 'member role must be string' };
    }
    if (m.workflow !== undefined && typeof m.workflow !== 'string') {
      return { success: false, error: 'member workflow must be string' };
    }
    members.push({
      name,
      role: typeof m.role === 'string' ? m.role.trim() : undefined,
      workflow: typeof m.workflow === 'string' ? m.workflow.trim() : undefined,
    });
  }

  return wrapTeamHandler('replaceMembers', async () => {
    await getTeamDataService().replaceMembers(vTeam.value!, { members });
  });
}

async function handleRemoveMember(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  memberName: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vMember = validateMemberName(memberName);
  if (!vMember.valid) return { success: false, error: vMember.error ?? 'Invalid memberName' };

  return wrapTeamHandler('removeMember', async () => {
    const tn = vTeam.value!;
    const name = vMember.value!;
    await getTeamDataService().removeMember(tn, name);

    // Notify the lead about removed member
    const provisioning = getTeamProvisioningService();
    if (provisioning.isTeamAlive(tn)) {
      const message =
        `Teammate "${name}" has been removed from the team. ` +
        `They will no longer participate in team activities. Please reassign their tasks if needed.`;
      try {
        await provisioning.sendMessageToTeam(tn, message);
      } catch {
        logger.warn(`Failed to notify lead about removal of "${name}" in ${tn}`);
      }
    }
  });
}

async function handleUpdateTaskFields(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  fields: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vTask = validateTaskId(taskId);
  if (!vTask.valid) return { success: false, error: vTask.error ?? 'Invalid taskId' };
  const tid = vTask.value!;
  if (!fields || typeof fields !== 'object') {
    return { success: false, error: 'fields must be an object' };
  }
  const { subject, description } = fields as { subject?: unknown; description?: unknown };
  if (subject !== undefined) {
    if (typeof subject !== 'string') return { success: false, error: 'subject must be a string' };
    if (subject.trim().length === 0) return { success: false, error: 'subject cannot be empty' };
    if (subject.length > 500)
      return { success: false, error: 'subject must be 500 characters or less' };
  }
  if (description !== undefined && typeof description !== 'string') {
    return { success: false, error: 'description must be a string' };
  }

  const validFields: { subject?: string; description?: string } = {};
  if (typeof subject === 'string') validFields.subject = subject.trim();
  if (typeof description === 'string') validFields.description = description;

  if (Object.keys(validFields).length === 0) {
    return { success: false, error: 'At least one field must be provided' };
  }

  return wrapTeamHandler('updateTaskFields', async () => {
    const tn = vTeam.value!;
    await getTeamDataService().updateTaskFields(tn, tid, validFields);

    // Notify the lead about updated task fields
    const provisioning = getTeamProvisioningService();
    if (provisioning.isTeamAlive(tn)) {
      const changedParts: string[] = [];
      if (validFields.subject) changedParts.push('title');
      if (validFields.description !== undefined) changedParts.push('description');
      const message =
        `Task #${tid} has been updated by the user (changed: ${changedParts.join(', ')}). ` +
        `New title: "${validFields.subject ?? '(unchanged)'}".`;
      try {
        await provisioning.sendMessageToTeam(tn, message);
      } catch {
        logger.warn(`Failed to notify lead about task fields update for #${tid} in ${tn}`);
      }
    }
  });
}

async function handleUpdateMemberRole(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  memberName: unknown,
  role: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vMember = validateMemberName(memberName);
  if (!vMember.valid) return { success: false, error: vMember.error ?? 'Invalid memberName' };

  const normalizedRole =
    role === undefined || role === null
      ? undefined
      : typeof role === 'string'
        ? role.trim() || undefined
        : undefined;

  return wrapTeamHandler('updateMemberRole', async () => {
    const tn = vTeam.value!;
    const name = vMember.value!;
    const { oldRole, changed } = await getTeamDataService().updateMemberRole(
      tn,
      name,
      normalizedRole
    );

    if (changed) {
      const provisioning = getTeamProvisioningService();
      if (provisioning.isTeamAlive(tn)) {
        const oldDesc = oldRole ? `"${oldRole}"` : 'none';
        const newDesc = normalizedRole ? `"${normalizedRole}"` : 'none';
        const message = `Teammate "${name}" role changed from ${oldDesc} to ${newDesc}. This will take effect on next launch.`;
        try {
          await provisioning.sendMessageToTeam(tn, message);
        } catch {
          logger.warn(`Failed to notify lead about role change for "${name}" in ${tn}`);
        }
      }
    }
  });
}

async function handleKillProcess(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  pid: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  if (typeof pid !== 'number' || !Number.isInteger(pid) || pid <= 0) {
    return { success: false, error: 'pid must be a positive integer' };
  }
  return wrapTeamHandler('killProcess', async () => {
    const tn = vTeam.value!;
    const pidNum = pid;

    // Read process label before killing (for notification message)
    let processLabel = `PID ${pidNum}`;
    try {
      const data = await getTeamDataService().getTeamData(tn);
      const proc = data.processes?.find((p) => p.pid === pidNum);
      if (proc) {
        processLabel = proc.label + (proc.port != null ? ` (:${proc.port})` : '');
      }
    } catch {
      // best-effort label lookup
    }

    await getTeamDataService().killProcess(tn, pidNum);

    // Notify the team lead about the killed process
    const provisioning = getTeamProvisioningService();
    if (provisioning.isTeamAlive(tn)) {
      const message =
        `Process "${processLabel}" (PID ${pidNum}) has been stopped by the user from the UI. ` +
        `You may need to restart it if it was still needed.`;
      try {
        await provisioning.sendMessageToTeam(tn, message);
      } catch {
        logger.warn(`Failed to notify lead about killed process ${pidNum} in ${tn}`);
      }
    }
  });
}

async function handleShowMessageNotification(
  _event: IpcMainInvokeEvent,
  data: unknown
): Promise<IpcResult<void>> {
  if (!data || typeof data !== 'object') {
    return { success: false, error: 'Invalid notification data' };
  }
  const d = data as TeamMessageNotificationData;
  if (!d.teamDisplayName || !d.from || !d.body) {
    return { success: false, error: 'Missing required fields (teamDisplayName, from, body)' };
  }
  if (!d.teamName) {
    return {
      success: false,
      error: 'Missing required field: teamName (needed for deep-link navigation)',
    };
  }

  // Route through NotificationManager for unified storage + native toast.
  // dedupeKey is required from renderer — built from stable identifiers (taskId, teamName, etc.)
  const dedupeKey =
    d.dedupeKey ?? `msg:${d.teamName}:${d.from}:${d.summary ?? d.body.slice(0, 50)}`;

  void NotificationManager.getInstance()
    .addTeamNotification({
      teamEventType: d.teamEventType ?? 'task_clarification',
      teamName: d.teamName,
      teamDisplayName: d.teamDisplayName,
      from: d.from,
      to: d.to,
      summary: d.summary ?? `${d.from} → ${d.to ?? 'team'}`,
      body: d.body,
      dedupeKey,
      suppressToast: d.suppressToast,
    })
    .catch(() => undefined);

  return { success: true, data: undefined };
}

/**
 * Show a native OS notification for a team event.
 * @deprecated Use NotificationManager.addTeamNotification() instead for unified storage + toast.
 * Kept for backward compatibility with any remaining callers.
 */
export function showTeamNativeNotification(opts: {
  title: string;
  subtitle?: string;
  body: string;
}): void {
  const config = ConfigManager.getInstance().getConfig();
  if (!config.notifications.enabled) {
    logger.debug('[native-notification] skipped: notifications disabled');
    return;
  }
  if (config.notifications.snoozedUntil && Date.now() < config.notifications.snoozedUntil) {
    logger.debug('[native-notification] skipped: snoozed');
    return;
  }

  if (
    typeof Notification === 'undefined' ||
    typeof Notification.isSupported !== 'function' ||
    !Notification.isSupported()
  ) {
    logger.warn('[native-notification] skipped: Notification not supported on this platform');
    return;
  }

  const isMac = process.platform === 'darwin';
  const truncatedBody = stripMarkdown(opts.body).slice(0, 300);
  const iconPath = isMac ? undefined : getAppIconPath();
  const notification = new Notification({
    title: opts.title,
    ...(isMac && opts.subtitle ? { subtitle: opts.subtitle } : {}),
    body: !isMac && opts.subtitle ? `${opts.subtitle}\n${truncatedBody}` : truncatedBody,
    sound: config.notifications.soundEnabled ? 'default' : undefined,
    ...(iconPath ? { icon: iconPath } : {}),
  });

  notification.on('click', () => {
    const windows = BrowserWindow.getAllWindows();
    const mainWin = windows[0];
    if (mainWin && !mainWin.isDestroyed()) {
      mainWin.show();
      mainWin.focus();
    }
  });

  notification.on('show', () => {
    logger.debug(`[native-notification] shown: "${opts.title}" — ${opts.subtitle ?? ''}`);
  });

  notification.on('failed', (_, error) => {
    logger.warn(`[native-notification] failed: ${error}`);
  });

  notification.show();
}

async function handleAddTaskComment(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  text: unknown,
  attachments?: unknown
): Promise<IpcResult<TaskComment>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vTask = validateTaskId(taskId);
  if (!vTask.valid) return { success: false, error: vTask.error ?? 'Invalid taskId' };
  if (typeof text !== 'string' || text.trim().length === 0)
    return { success: false, error: 'Comment text must be non-empty' };
  if (text.trim().length > MAX_TEXT_LENGTH)
    return { success: false, error: `Comment exceeds ${MAX_TEXT_LENGTH} characters` };

  const rawAttachments = Array.isArray(attachments) ? attachments : [];
  if (rawAttachments.length > MAX_ATTACHMENTS) {
    return { success: false, error: `Maximum ${MAX_ATTACHMENTS} attachments per comment` };
  }

  return wrapTeamHandler('addTaskComment', async () => {
    // Save comment attachments (images). Done inside wrapTeamHandler so failures return IpcResult.
    let savedAttachments: TaskAttachmentMeta[] | undefined;
    if (rawAttachments.length > 0) {
      savedAttachments = [];
      for (const att of rawAttachments) {
        if (!att || typeof att !== 'object') {
          throw new Error('Invalid attachment data');
        }
        const a = att as Record<string, unknown>;
        if (
          typeof a.id !== 'string' ||
          typeof a.filename !== 'string' ||
          typeof a.mimeType !== 'string' ||
          typeof a.base64Data !== 'string' ||
          a.base64Data.length === 0 ||
          !ALLOWED_ATTACHMENT_TYPES.has(a.mimeType)
        ) {
          throw new Error('Invalid attachment data');
        }
        const safeId = a.id.trim();
        if (safeId.includes('/') || safeId.includes('\\') || safeId.includes('..')) {
          throw new Error('Invalid attachment ID');
        }
        const meta = await taskAttachmentStore.saveAttachment(
          vTeam.value!,
          vTask.value!,
          safeId,
          a.filename,
          a.mimeType,
          a.base64Data
        );
        savedAttachments.push(meta);
      }
    }

    return getTeamDataService().addTaskComment(
      vTeam.value!,
      vTask.value!,
      text.trim(),
      savedAttachments
    );
  });
}

const VALID_RELATIONSHIP_TYPES = ['blockedBy', 'blocks', 'related'] as const;
type RelationshipType = (typeof VALID_RELATIONSHIP_TYPES)[number];

async function handleAddTaskRelationship(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  targetId: unknown,
  type: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vTask = validateTaskId(taskId);
  if (!vTask.valid) return { success: false, error: vTask.error ?? 'Invalid taskId' };
  const vTarget = validateTaskId(targetId);
  if (!vTarget.valid) return { success: false, error: vTarget.error ?? 'Invalid targetId' };
  if (typeof type !== 'string' || !VALID_RELATIONSHIP_TYPES.includes(type as RelationshipType)) {
    return {
      success: false,
      error: `type must be one of: ${VALID_RELATIONSHIP_TYPES.join(', ')}`,
    };
  }

  return wrapTeamHandler('addTaskRelationship', () =>
    getTeamDataService().addTaskRelationship(
      vTeam.value!,
      vTask.value!,
      vTarget.value!,
      type as RelationshipType
    )
  );
}

async function handleRemoveTaskRelationship(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  targetId: unknown,
  type: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vTask = validateTaskId(taskId);
  if (!vTask.valid) return { success: false, error: vTask.error ?? 'Invalid taskId' };
  const vTarget = validateTaskId(targetId);
  if (!vTarget.valid) return { success: false, error: vTarget.error ?? 'Invalid targetId' };
  if (typeof type !== 'string' || !VALID_RELATIONSHIP_TYPES.includes(type as RelationshipType)) {
    return {
      success: false,
      error: `type must be one of: ${VALID_RELATIONSHIP_TYPES.join(', ')}`,
    };
  }

  return wrapTeamHandler('removeTaskRelationship', () =>
    getTeamDataService().removeTaskRelationship(
      vTeam.value!,
      vTask.value!,
      vTarget.value!,
      type as RelationshipType
    )
  );
}

// ---------------------------------------------------------------------------
// Task Attachment Handlers
// ---------------------------------------------------------------------------

async function handleSaveTaskAttachment(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  attachmentId: unknown,
  filename: unknown,
  mimeType: unknown,
  base64Data: unknown
): Promise<IpcResult<TaskAttachmentMeta>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vTask = validateTaskId(taskId);
  if (!vTask.valid) return { success: false, error: vTask.error ?? 'Invalid taskId' };
  if (typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
    return { success: false, error: 'attachmentId must be a non-empty string' };
  }
  if (typeof filename !== 'string' || filename.trim().length === 0) {
    return { success: false, error: 'filename must be a non-empty string' };
  }
  if (typeof mimeType !== 'string' || !ALLOWED_ATTACHMENT_TYPES.has(mimeType)) {
    return {
      success: false,
      error: `mimeType must be one of: ${[...ALLOWED_ATTACHMENT_TYPES].join(', ')}`,
    };
  }
  if (typeof base64Data !== 'string' || base64Data.length === 0) {
    return { success: false, error: 'base64Data must be a non-empty string' };
  }
  // Sanitize IDs against path traversal
  const safeAttId = attachmentId.trim();
  if (safeAttId.includes('/') || safeAttId.includes('\\') || safeAttId.includes('..')) {
    return { success: false, error: 'Invalid attachmentId' };
  }

  return wrapTeamHandler('saveTaskAttachment', async () => {
    const meta = await taskAttachmentStore.saveAttachment(
      vTeam.value!,
      vTask.value!,
      safeAttId,
      filename,
      mimeType,
      base64Data
    );
    // Write metadata into the task JSON
    await getTeamDataService().addTaskAttachment(vTeam.value!, vTask.value!, meta);
    return meta;
  });
}

async function handleGetTaskAttachment(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  attachmentId: unknown,
  mimeType: unknown
): Promise<IpcResult<string | null>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vTask = validateTaskId(taskId);
  if (!vTask.valid) return { success: false, error: vTask.error ?? 'Invalid taskId' };
  if (typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
    return { success: false, error: 'attachmentId must be a non-empty string' };
  }
  if (typeof mimeType !== 'string' || !ALLOWED_ATTACHMENT_TYPES.has(mimeType)) {
    return { success: false, error: 'Invalid mimeType' };
  }
  const safeAttId = attachmentId.trim();
  if (safeAttId.includes('/') || safeAttId.includes('\\') || safeAttId.includes('..')) {
    return { success: false, error: 'Invalid attachmentId' };
  }

  return wrapTeamHandler('getTaskAttachment', () =>
    taskAttachmentStore.getAttachment(vTeam.value!, vTask.value!, safeAttId, mimeType)
  );
}

async function handleDeleteTaskAttachment(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  taskId: unknown,
  attachmentId: unknown,
  mimeType: unknown
): Promise<IpcResult<void>> {
  const vTeam = validateTeamName(teamName);
  if (!vTeam.valid) return { success: false, error: vTeam.error ?? 'Invalid teamName' };
  const vTask = validateTaskId(taskId);
  if (!vTask.valid) return { success: false, error: vTask.error ?? 'Invalid taskId' };
  if (typeof attachmentId !== 'string' || attachmentId.trim().length === 0) {
    return { success: false, error: 'attachmentId must be a non-empty string' };
  }
  if (typeof mimeType !== 'string' || !ALLOWED_ATTACHMENT_TYPES.has(mimeType)) {
    return { success: false, error: 'Invalid mimeType' };
  }
  const safeAttId = attachmentId.trim();
  if (safeAttId.includes('/') || safeAttId.includes('\\') || safeAttId.includes('..')) {
    return { success: false, error: 'Invalid attachmentId' };
  }

  return wrapTeamHandler('deleteTaskAttachment', async () => {
    await taskAttachmentStore.deleteAttachment(vTeam.value!, vTask.value!, safeAttId, mimeType);
    // Remove metadata from task JSON
    await getTeamDataService().removeTaskAttachment(vTeam.value!, vTask.value!, safeAttId);
  });
}

async function handleToolApprovalRespond(
  _event: IpcMainInvokeEvent,
  teamName: unknown,
  runId: unknown,
  requestId: unknown,
  allow: unknown,
  message?: unknown
): Promise<IpcResult<void>> {
  const validated = validateTeamName(teamName);
  if (!validated.valid) {
    return { success: false, error: validated.error ?? 'Invalid teamName' };
  }
  if (typeof runId !== 'string' || runId.trim().length === 0) {
    return { success: false, error: 'runId must be a non-empty string' };
  }
  if (typeof requestId !== 'string' || requestId.trim().length === 0) {
    return { success: false, error: 'requestId must be a non-empty string' };
  }
  if (typeof allow !== 'boolean') {
    return { success: false, error: 'allow must be a boolean' };
  }
  return wrapTeamHandler('toolApprovalRespond', () =>
    getTeamProvisioningService().respondToToolApproval(
      validated.value!,
      runId,
      requestId,
      allow,
      typeof message === 'string' ? message : undefined
    )
  );
}

async function handleToolApprovalSettings(
  _event: IpcMainInvokeEvent,
  settings: unknown
): Promise<IpcResult<void>> {
  if (typeof settings !== 'object' || settings === null) {
    return { success: false, error: 'Settings must be an object' };
  }
  const s = settings as Record<string, unknown>;
  if (typeof s.autoAllowFileEdits !== 'boolean') {
    return { success: false, error: 'autoAllowFileEdits must be a boolean' };
  }
  if (typeof s.autoAllowSafeBash !== 'boolean') {
    return { success: false, error: 'autoAllowSafeBash must be a boolean' };
  }
  if (typeof s.timeoutAction !== 'string' || !['allow', 'deny', 'wait'].includes(s.timeoutAction)) {
    return { success: false, error: 'timeoutAction must be "allow", "deny", or "wait"' };
  }
  if (
    typeof s.timeoutSeconds !== 'number' ||
    !Number.isFinite(s.timeoutSeconds) ||
    s.timeoutSeconds < 5 ||
    s.timeoutSeconds > 300
  ) {
    return { success: false, error: 'timeoutSeconds must be a number between 5 and 300' };
  }

  try {
    getTeamProvisioningService().updateToolApprovalSettings(s as unknown as ToolApprovalSettings);
  } catch (err) {
    return {
      success: false,
      error: `Failed to update tool approval settings: ${err instanceof Error ? err.message : String(err)}`,
    };
  }
  return { success: true, data: undefined };
}
