// Copyright (c) 2026 Hellmai Ltd
// SPDX-License-Identifier: AGPL-3.0-only

/**
 * Auto-Session Integration for wu:claim and wu:done lifecycle (WU-1438, WU-1466)
 *
 * Provides wrapper functions around agent-session.ts that:
 * 1. Auto-start sessions on wu:claim with silent no-op if already active
 * 2. Auto-end sessions on wu:done with silent no-op if not active
 * 3. Store session_id in WU YAML for tracking
 * 4. Create memory layer session nodes for context restoration (WU-1466)
 *
 * Design principles:
 * - Composition over modification (wraps existing agent-session.ts)
 * - Silent failures for idempotent operations (no throw on duplicate start/end)
 * - Configurable session directory for testing
 */
import { randomUUID } from 'crypto';
import { readFileSync, writeFileSync, existsSync, unlinkSync, mkdirSync, readdirSync } from 'fs';
import { join } from 'path';
import { parse as parseYAML } from 'yaml';
import {
  HeartbeatManager,
  type AgentHeartbeatInput,
  type AgentHeartbeatResult,
  type HeartbeatHealth,
} from './agent-heartbeat.js';
import {
  acquireDisplayName,
  releaseDisplayName,
  SESSION_SCHEMA_VERSION_V2,
  withV2RoleDefaults,
  type SessionSchemaVersion,
} from './session-schema.js';

const SESSION_FILENAME = 'current.json';
const SESSION_DIR = '.lumenflow/sessions';
const WORKSPACE_CONFIG_FILE = 'workspace.yaml';
const CONTROL_PLANE_REGISTER_PATH = '/api/v1/sessions/register';
const CONTROL_PLANE_DEREGISTER_PATH = '/api/v1/sessions/deregister';
const CONTROL_PLANE_HEARTBEAT_PATH = '/api/v1/heartbeat';

// Default context tier for auto-started sessions
const DEFAULT_TIER: 1 | 2 | 3 = 2;

// Agent type for auto-started sessions
const DEFAULT_AGENT_TYPE = 'claude-code';
const DEFAULT_AGENT_VERSION = 'unknown';
const DEFAULT_HOST_ID = 'unknown';
const DEFAULT_AGENT_CAPABILITIES = ['session_lifecycle', 'heartbeat'] as const;
const AGENT_CAPABILITIES_ENV = 'LUMENFLOW_AGENT_CAPABILITIES';
const AGENT_VERSION_ENV = 'LUMENFLOW_AGENT_VERSION';

type SessionMetadataValue = string | number | boolean | string[];

/**
 * Session data stored in current.json
 *
 * WU-2754 (ADR-014 extension 2): `schemaVersion`, `lifecycle_role`,
 * `specialty_profile`, and `delegation_id` are optional additive fields.
 * v1 legacy records (no `schemaVersion`) continue to load without them.
 */
interface SessionFileData {
  session_id: string;
  wu_id: string;
  started: string;
  last_heartbeat?: string;
  completed?: string;
  lane?: string;
  agent_type: string;
  client_type?: string;
  capabilities?: string[];
  agent_version?: string;
  host_id?: string;
  context_tier: number;
  incidents_logged: number;
  incidents_major: number;
  auto_started?: boolean;
  /** ADR-014 extension 2 — schema version stamp for v2 records (WU-2754). */
  schemaVersion?: SessionSchemaVersion;
  /** ADR-014 extension 2 — what phase of delivery this session owns (WU-2754). */
  lifecycle_role?: string;
  /** ADR-014 extension 2 — which skill bundle the agent identifies as carrying (WU-2754). */
  specialty_profile?: string;
  display_name?: string;
  /** ADR-014 extension 2 — parent delegation registry reference (WU-2754). */
  delegation_id?: string;
}

interface RegisterSessionInput {
  workspace_id: string;
  session_id: string;
  agent_id: string;
  started_at: string;
  lane?: string;
  wu_id?: string;
  client_type?: string;
  capabilities?: string[];
  agent_version?: string;
  host_id?: string;
  metadata?: Record<string, SessionMetadataValue>;
}

interface DeregisterSessionInput {
  workspace_id: string;
  session_id: string;
  ended_at?: string;
  reason?: string;
}

interface ControlPlaneSessionSyncPort {
  registerSession(input: RegisterSessionInput): Promise<unknown>;
  deregisterSession(input: DeregisterSessionInput): Promise<unknown>;
  heartbeat?(input: AgentHeartbeatInput): Promise<AgentHeartbeatResult>;
}

interface ResolvedControlPlaneSyncConfig {
  workspaceId: string;
  endpoint: string;
  token: string;
}

interface WorkspaceControlPlaneDocument {
  id?: unknown;
  control_plane?: {
    endpoint?: unknown;
    auth?: {
      token_env?: unknown;
    };
  };
}

function parseCapabilities(rawValue: string | null): string[] {
  if (!rawValue) {
    return [...DEFAULT_AGENT_CAPABILITIES];
  }

  const parsed = rawValue
    .split(',')
    .map((entry) => entry.trim())
    .filter((entry) => entry.length > 0);
  return parsed.length > 0 ? parsed : [...DEFAULT_AGENT_CAPABILITIES];
}

function asNonEmptyString(value: unknown): string | null {
  return typeof value === 'string' && value.trim().length > 0 ? value.trim() : null;
}

function normalizeEndpoint(endpoint: string): string {
  return endpoint.endsWith('/') ? endpoint.slice(0, endpoint.length - 1) : endpoint;
}

function resolveControlPlaneSessionSyncConfig(
  workspaceRoot: string,
  environment: NodeJS.ProcessEnv,
): ResolvedControlPlaneSyncConfig | null {
  const workspacePath = join(workspaceRoot, WORKSPACE_CONFIG_FILE);
  if (!existsSync(workspacePath)) {
    return null;
  }

  let parsedWorkspace: WorkspaceControlPlaneDocument | null;
  try {
    parsedWorkspace = parseYAML(
      readFileSync(workspacePath, { encoding: 'utf-8' }),
    ) as WorkspaceControlPlaneDocument | null;
  } catch {
    return null;
  }

  const workspaceId = asNonEmptyString(parsedWorkspace?.id);
  const endpoint = asNonEmptyString(parsedWorkspace?.control_plane?.endpoint);
  const tokenEnv = asNonEmptyString(parsedWorkspace?.control_plane?.auth?.token_env);
  if (!workspaceId || !endpoint || !tokenEnv) {
    return null;
  }

  const token = asNonEmptyString(environment[tokenEnv]);
  if (!token) {
    return null;
  }

  try {
    // Validate endpoint before creating outbound requests.
    void new URL(endpoint);
  } catch {
    return null;
  }

  return {
    workspaceId,
    endpoint: normalizeEndpoint(endpoint),
    token,
  };
}

function resolveStandardSessionMetadata(input: {
  agentType: string;
  environment: NodeJS.ProcessEnv;
}): {
  client_type: string;
  capabilities: string[];
  agent_version: string;
  host_id: string;
  metadata: Record<string, SessionMetadataValue>;
} {
  const clientType = input.agentType;
  const capabilities = parseCapabilities(
    asNonEmptyString(input.environment[AGENT_CAPABILITIES_ENV]),
  );
  const agentVersion =
    asNonEmptyString(input.environment[AGENT_VERSION_ENV]) ?? DEFAULT_AGENT_VERSION;
  const hostId =
    asNonEmptyString(input.environment.HOSTNAME) ??
    asNonEmptyString(input.environment.COMPUTERNAME) ??
    DEFAULT_HOST_ID;

  return {
    client_type: clientType,
    capabilities,
    agent_version: agentVersion,
    host_id: hostId,
    metadata: {
      client_type: clientType,
      capabilities,
      agent_version: agentVersion,
      host_id: hostId,
    },
  };
}

async function postControlPlaneSessionHook(
  endpoint: string,
  token: string,
  path: string,
  payload: object,
  fetchFn: typeof fetch,
): Promise<void> {
  const response = await fetchFn(`${endpoint}${path}`, {
    method: 'POST',
    headers: {
      authorization: `Bearer ${token}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify(payload),
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }
}

function asFiniteNumber(value: unknown): number | undefined {
  return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
}

function normalizeSessionFileData(session: SessionFileData): SessionFileData {
  return {
    ...session,
    client_type: session.client_type ?? session.agent_type,
    capabilities: Array.isArray(session.capabilities)
      ? session.capabilities.filter((entry): entry is string => typeof entry === 'string')
      : [...DEFAULT_AGENT_CAPABILITIES],
    agent_version: session.agent_version ?? DEFAULT_AGENT_VERSION,
    host_id: session.host_id ?? DEFAULT_HOST_ID,
  };
}

function normalizeHeartbeatResult(
  raw: AgentHeartbeatResult | Record<string, unknown>,
): AgentHeartbeatResult {
  return {
    status: 'ok',
    server_time:
      typeof raw.server_time === 'string' && raw.server_time.length > 0
        ? raw.server_time
        : new Date().toISOString(),
    ...(asFiniteNumber(raw.next_heartbeat_ms) !== undefined
      ? { next_heartbeat_ms: asFiniteNumber(raw.next_heartbeat_ms) }
      : {}),
    ...(asFiniteNumber(raw.budget_remaining_usd) !== undefined
      ? { budget_remaining_usd: asFiniteNumber(raw.budget_remaining_usd) }
      : {}),
    ...(asFiniteNumber(raw.coalesced_signals) !== undefined
      ? { coalesced_signals: asFiniteNumber(raw.coalesced_signals) }
      : {}),
    ...(typeof raw.assignment === 'object' && raw.assignment !== null
      ? { assignment: raw.assignment as AgentHeartbeatResult['assignment'] }
      : {}),
  };
}

async function postControlPlaneHeartbeat(
  endpoint: string,
  token: string,
  payload: AgentHeartbeatInput,
  fetchFn: typeof fetch,
): Promise<AgentHeartbeatResult> {
  const response = await fetchFn(`${endpoint}${CONTROL_PLANE_HEARTBEAT_PATH}`, {
    method: 'POST',
    headers: {
      authorization: `Bearer ${token}`,
      'content-type': 'application/json',
    },
    body: JSON.stringify(payload),
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}`);
  }

  const raw = (await response.json().catch(() => ({}))) as AgentHeartbeatResult;
  return normalizeHeartbeatResult(raw);
}

/**
 * Get the session file path for a given session directory
 * @param sessionDir - Session directory path
 * @returns Full path to current.json
 */
function getSessionFilePath(sessionDir: string): string {
  return join(sessionDir, SESSION_FILENAME);
}

function getCanonicalSessionFilePath(sessionDir: string, wuId: string): string {
  return join(sessionDir, `${wuId}.json`);
}

function ensureSessionDirectory(sessionDir: string): void {
  if (!existsSync(sessionDir)) {
    mkdirSync(sessionDir, { recursive: true });
  }
}

function readSessionFile(filePath: string): SessionFileData | null {
  if (!existsSync(filePath)) {
    return null;
  }

  return normalizeSessionFileData(
    JSON.parse(readFileSync(filePath, { encoding: 'utf-8' })) as SessionFileData,
  );
}

function writeSessionFile(filePath: string, session: SessionFileData): void {
  writeFileSync(filePath, JSON.stringify(session, null, 2), { encoding: 'utf-8' });
}

function getCanonicalSessions(sessionDir: string): SessionFileData[] {
  if (!existsSync(sessionDir)) {
    return [];
  }

  return readdirSync(sessionDir)
    .filter((entry) => entry.endsWith('.json') && entry !== SESSION_FILENAME)
    .map((entry) => readSessionFile(join(sessionDir, entry)))
    .filter((session): session is SessionFileData => session !== null);
}

function getMostRecentActiveSession(sessionDir: string): SessionFileData | null {
  const sessions = getCanonicalSessions(sessionDir);
  if (sessions.length === 0) {
    return null;
  }

  const [latest] = [...sessions].sort((left, right) => {
    const leftTime = Date.parse(left.started);
    const rightTime = Date.parse(right.started);

    if (Number.isNaN(leftTime) && Number.isNaN(rightTime)) {
      return left.wu_id.localeCompare(right.wu_id);
    }
    if (Number.isNaN(leftTime)) {
      return 1;
    }
    if (Number.isNaN(rightTime)) {
      return -1;
    }

    return rightTime - leftTime;
  });

  return latest ?? null;
}

function refreshCompatibilityPointer(
  sessionDir: string,
  preferredSession: SessionFileData | null = null,
): void {
  const compatibilityPath = getSessionFilePath(sessionDir);
  const activeSession = preferredSession ?? getMostRecentActiveSession(sessionDir);

  if (!activeSession) {
    if (existsSync(compatibilityPath)) {
      unlinkSync(compatibilityPath);
    }
    return;
  }

  ensureSessionDirectory(sessionDir);
  writeSessionFile(compatibilityPath, activeSession);
}

function resolveSessionForWU(sessionDir: string, wuId: string): SessionFileData | null {
  const canonicalSession = readSessionFile(getCanonicalSessionFilePath(sessionDir, wuId));
  if (canonicalSession) {
    return canonicalSession;
  }

  const compatibilitySession = readSessionFile(getSessionFilePath(sessionDir));
  if (compatibilitySession?.wu_id === wuId) {
    ensureSessionDirectory(sessionDir);
    writeSessionFile(getCanonicalSessionFilePath(sessionDir, wuId), compatibilitySession);
    refreshCompatibilityPointer(sessionDir, compatibilitySession);
    return compatibilitySession;
  }

  return null;
}

function resolveCurrentCompatibilitySession(sessionDir: string): SessionFileData | null {
  const compatibilitySession = readSessionFile(getSessionFilePath(sessionDir));
  if (compatibilitySession) {
    return compatibilitySession;
  }

  const latestSession = getMostRecentActiveSession(sessionDir);
  if (latestSession) {
    refreshCompatibilityPointer(sessionDir, latestSession);
  }
  return latestSession;
}

/**
 * Options for starting a session for a WU
 *
 * WU-2754: `lifecycleRole`, `specialtyProfile`, `delegationId` are optional
 * ADR-014 extension 2 axes. When omitted, `startSessionForWU` applies
 * kernel-agnostic v2 defaults (executor/general) — the CLI pack layer owns
 * the canonical vocabulary (see `delegation-role-resolver.ts`).
 */
interface StartSessionOptions {
  wuId: string;
  tier?: 1 | 2 | 3;
  agentType?: string;
  lane?: string;
  sessionDir?: string;
  workspaceRoot?: string;
  environment?: NodeJS.ProcessEnv;
  controlPlaneSyncPort?: ControlPlaneSessionSyncPort;
  fetchFn?: typeof fetch;
  baseDir?: string;
  /** ADR-014 lifecycle_role for this session (e.g. 'orchestrator'). */
  lifecycleRole?: string;
  /** ADR-014 specialty_profile for this session (e.g. 'delivery'). */
  specialtyProfile?: string;
  displayName?: string;
  /** Parent delegation-registry record reference (dlg-XXXX). */
  delegationId?: string;
}

/**
 * Result of starting a session
 */
interface StartSessionResult {
  sessionId: string;
  alreadyActive?: boolean;
  memoryNodeId?: string | null;
}

interface HeartbeatSessionOptions {
  wuId?: string;
  sessionDir?: string;
  workspaceRoot?: string;
  environment?: NodeJS.ProcessEnv;
  controlPlaneSyncPort?: ControlPlaneSessionSyncPort;
  heartbeatManager?: HeartbeatManager;
  fetchFn?: typeof fetch;
  health?: HeartbeatHealth;
}

interface HeartbeatSessionResult {
  sent: boolean;
  reason?: 'no_active_session' | 'control_plane_unavailable';
  heartbeat?: AgentHeartbeatResult;
}

/**
 * Start a session for a WU (called by wu:claim)
 *
 * Unlike startSession in agent-session.ts, this function:
 * - Does NOT throw if a session already exists (returns existing session)
 * - Uses default tier 2 if not specified
 * - Supports custom session directory for testing
 * - Creates memory layer session node for context restoration (WU-1466)
 *
 * @param options - Session options
 * @returns Session result
 */
export async function startSessionForWU(options: StartSessionOptions): Promise<StartSessionResult> {
  const {
    wuId,
    tier = DEFAULT_TIER,
    agentType = DEFAULT_AGENT_TYPE,
    lane,
    sessionDir,
    workspaceRoot = process.cwd(),
    environment = process.env,
    controlPlaneSyncPort,
    fetchFn = fetch,
    lifecycleRole,
    specialtyProfile,
    displayName,
    delegationId,
  } = options;

  const sessDir = sessionDir ?? SESSION_DIR;
  const canonicalSessionFile = getCanonicalSessionFilePath(sessDir, wuId);

  const existing = resolveSessionForWU(sessDir, wuId);
  if (existing) {
    refreshCompatibilityPointer(sessDir, existing);
    return {
      sessionId: existing.session_id,
      alreadyActive: true,
    };
  }

  // Create session directory if needed
  ensureSessionDirectory(sessDir);

  const standardizedMetadata = resolveStandardSessionMetadata({
    agentType,
    environment,
  });

  // Create new session
  const sessionId = randomUUID();
  // WU-2754 (ADR-014 extension 2): every new session is v2-stamped and
  // carries lifecycle_role + specialty_profile (caller-supplied or defaulted).
  const roleAxes = withV2RoleDefaults({
    lifecycle_role: lifecycleRole,
    specialty_profile: specialtyProfile,
    display_name: displayName,
    delegation_id: delegationId,
  });
  const resolvedDisplayName = roleAxes.display_name ?? acquireDisplayName(workspaceRoot, sessionId);
  const session: SessionFileData = {
    session_id: sessionId,
    wu_id: wuId,
    started: new Date().toISOString(),
    last_heartbeat: new Date().toISOString(),
    lane,
    agent_type: agentType,
    client_type: standardizedMetadata.client_type,
    capabilities: standardizedMetadata.capabilities,
    agent_version: standardizedMetadata.agent_version,
    host_id: standardizedMetadata.host_id,
    context_tier: tier,
    incidents_logged: 0,
    incidents_major: 0,
    auto_started: true, // Mark as auto-started by wu:claim
    schemaVersion: SESSION_SCHEMA_VERSION_V2,
    lifecycle_role: roleAxes.lifecycle_role,
    specialty_profile: roleAxes.specialty_profile,
    display_name: resolvedDisplayName,
    ...(roleAxes.delegation_id ? { delegation_id: roleAxes.delegation_id } : {}),
  };

  writeSessionFile(canonicalSessionFile, session);
  refreshCompatibilityPointer(sessDir, session);

  // WU-2153: Optional control-plane session registration.
  // Fail-open by design: session lifecycle must not be blocked by remote errors.
  const controlPlaneConfig = resolveControlPlaneSessionSyncConfig(workspaceRoot, environment);
  if (controlPlaneConfig) {
    const registerInput: RegisterSessionInput = {
      workspace_id: controlPlaneConfig.workspaceId,
      session_id: sessionId,
      agent_id: agentType,
      started_at: session.started,
      lane,
      wu_id: wuId,
      client_type: standardizedMetadata.client_type,
      capabilities: standardizedMetadata.capabilities,
      agent_version: standardizedMetadata.agent_version,
      host_id: standardizedMetadata.host_id,
      metadata: standardizedMetadata.metadata,
    };

    try {
      if (controlPlaneSyncPort) {
        await controlPlaneSyncPort.registerSession(registerInput);
      } else {
        await postControlPlaneSessionHook(
          controlPlaneConfig.endpoint,
          controlPlaneConfig.token,
          CONTROL_PLANE_REGISTER_PATH,
          registerInput,
          fetchFn,
        );
      }
    } catch {
      // Fail-open: remote registration must not block wu:claim.
    }
  }

  return {
    sessionId,
    alreadyActive: false,
  };
}

/**
 * Send a control-plane heartbeat for the active WU session.
 *
 * This is fail-open for local workflows: if no active session or no control-plane config
 * is present, no heartbeat is sent and a structured reason is returned.
 */
export async function heartbeatSessionForWU(
  options: HeartbeatSessionOptions = {},
): Promise<HeartbeatSessionResult> {
  const {
    wuId,
    sessionDir,
    workspaceRoot = process.cwd(),
    environment = process.env,
    controlPlaneSyncPort,
    heartbeatManager,
    fetchFn = fetch,
    health,
  } = options;
  const sessDir = sessionDir ?? SESSION_DIR;

  const currentSession = getCurrentSessionForWU({ sessionDir, wuId });
  if (!currentSession) {
    return {
      sent: false,
      reason: 'no_active_session',
    };
  }

  const controlPlaneConfig = resolveControlPlaneSessionSyncConfig(workspaceRoot, environment);
  if (!controlPlaneConfig) {
    return {
      sent: false,
      reason: 'control_plane_unavailable',
    };
  }

  const heartbeatPort = controlPlaneSyncPort?.heartbeat
    ? {
        heartbeat: controlPlaneSyncPort.heartbeat.bind(controlPlaneSyncPort),
      }
    : {
        heartbeat: async (input: AgentHeartbeatInput): Promise<AgentHeartbeatResult> =>
          postControlPlaneHeartbeat(
            controlPlaneConfig.endpoint,
            controlPlaneConfig.token,
            input,
            fetchFn,
          ),
      };

  const manager = heartbeatManager ?? new HeartbeatManager(heartbeatPort);
  const heartbeat = await manager.heartbeat({
    workspace_id: controlPlaneConfig.workspaceId,
    session_id: currentSession.session_id,
    agent_id: currentSession.agent_type,
    wu_id: currentSession.wu_id,
    ...(health ? { health } : {}),
  });

  currentSession.last_heartbeat = new Date().toISOString();
  writeSessionFile(getCanonicalSessionFilePath(sessDir, currentSession.wu_id), currentSession);
  refreshCompatibilityPointer(sessDir, currentSession);

  return {
    sent: true,
    heartbeat,
  };
}

/**
 * Options for ending a session
 */
interface EndSessionOptions {
  wuId?: string;
  sessionDir?: string;
  workspaceRoot?: string;
  environment?: NodeJS.ProcessEnv;
  controlPlaneSyncPort?: ControlPlaneSessionSyncPort;
  fetchFn?: typeof fetch;
}

/**
 * Session summary
 *
 * WU-2754: includes optional ADR-014 extension 2 axes (lifecycle_role,
 * specialty_profile, delegation_id). Absent on v1 legacy sessions.
 */
interface SessionSummary {
  wu_id: string;
  session_id: string;
  started: string;
  completed: string;
  agent_type: string;
  context_tier: number;
  incidents_logged: number;
  incidents_major: number;
  /** ADR-014 extension 2 — present on v2 sessions (WU-2754). */
  lifecycle_role?: string;
  /** ADR-014 extension 2 — present on v2 sessions (WU-2754). */
  specialty_profile?: string;
  display_name?: string;
  /** ADR-014 extension 2 — present when session linked to a delegation (WU-2754). */
  delegation_id?: string;
  /** ADR-014 extension 2 — 'v2' when session was written with the v2 schema (WU-2754). */
  schemaVersion?: SessionSchemaVersion;
}

/**
 * Result of ending a session
 */
interface EndSessionResult {
  ended: boolean;
  summary?: SessionSummary;
  reason?: string;
}

/**
 * End the current session (called by wu:done)
 *
 * Unlike endSession in agent-session.ts, this function:
 * - Does NOT throw if no active session (returns { ended: false })
 * - Returns structured result with summary
 * - Supports custom session directory for testing
 *
 * @param options - Session options
 * @returns Session end result
 */
export function endSessionForWU(options: EndSessionOptions = {}): EndSessionResult {
  const {
    wuId,
    sessionDir,
    workspaceRoot = process.cwd(),
    environment = process.env,
    controlPlaneSyncPort,
    fetchFn = fetch,
  } = options;

  const sessDir = sessionDir ?? SESSION_DIR;
  const canonicalSessionFile =
    typeof wuId === 'string' && wuId.length > 0 ? getCanonicalSessionFilePath(sessDir, wuId) : null;
  const compatibilitySessionFile = getSessionFilePath(sessDir);
  const session =
    (typeof wuId === 'string' && wuId.length > 0
      ? resolveSessionForWU(sessDir, wuId)
      : resolveCurrentCompatibilitySession(sessDir)) ?? null;

  if (!session) {
    return {
      ended: false,
      reason: 'no_active_session',
    };
  }

  // Finalize session
  session.completed = new Date().toISOString();
  releaseDisplayName(workspaceRoot, session.session_id);

  // Build summary for WU YAML
  // WU-2754 (ADR-014 extension 2): include the three role axes when the
  // session was stamped v2 or otherwise carries them.
  const summary: SessionSummary = {
    wu_id: session.wu_id,
    session_id: session.session_id,
    started: session.started,
    completed: session.completed,
    agent_type: session.agent_type,
    context_tier: session.context_tier,
    incidents_logged: session.incidents_logged,
    incidents_major: session.incidents_major,
    ...(session.schemaVersion ? { schemaVersion: session.schemaVersion } : {}),
    ...(session.lifecycle_role ? { lifecycle_role: session.lifecycle_role } : {}),
    ...(session.specialty_profile ? { specialty_profile: session.specialty_profile } : {}),
    ...(session.display_name ? { display_name: session.display_name } : {}),
    ...(session.delegation_id ? { delegation_id: session.delegation_id } : {}),
  };

  const resolvedCanonicalSessionFile = getCanonicalSessionFilePath(sessDir, session.wu_id);
  if (existsSync(resolvedCanonicalSessionFile)) {
    unlinkSync(resolvedCanonicalSessionFile);
  }

  const compatibilitySession = readSessionFile(compatibilitySessionFile);
  if (
    compatibilitySession?.session_id === session.session_id &&
    existsSync(compatibilitySessionFile)
  ) {
    unlinkSync(compatibilitySessionFile);
  }

  if (canonicalSessionFile && existsSync(canonicalSessionFile)) {
    unlinkSync(canonicalSessionFile);
  }
  refreshCompatibilityPointer(sessDir);

  // WU-2153: Optional control-plane session deregistration.
  // Fail-open by design: completion path must not block on remote errors.
  const controlPlaneConfig = resolveControlPlaneSessionSyncConfig(workspaceRoot, environment);
  if (controlPlaneConfig) {
    const deregisterInput: DeregisterSessionInput = {
      workspace_id: controlPlaneConfig.workspaceId,
      session_id: session.session_id,
      ended_at: session.completed,
      reason: 'wu_done',
    };

    if (controlPlaneSyncPort) {
      void controlPlaneSyncPort.deregisterSession(deregisterInput).catch(() => {});
    } else {
      void postControlPlaneSessionHook(
        controlPlaneConfig.endpoint,
        controlPlaneConfig.token,
        CONTROL_PLANE_DEREGISTER_PATH,
        deregisterInput,
        fetchFn,
      ).catch(() => {});
    }
  }

  return {
    ended: true,
    summary,
  };
}

/**
 * Options for getting current session
 */
interface GetSessionOptions {
  wuId?: string;
  sessionDir?: string;
}

/**
 * Get the current active session
 *
 * @param options - Session options
 * @returns Session object or null if no active session
 */
export function getCurrentSessionForWU(options: GetSessionOptions = {}): SessionFileData | null {
  const { wuId, sessionDir } = options;

  const sessDir = sessionDir ?? SESSION_DIR;
  if (typeof wuId === 'string' && wuId.length > 0) {
    return resolveSessionForWU(sessDir, wuId);
  }

  return resolveCurrentCompatibilitySession(sessDir);
}

/**
 * Check if there's an active session for a specific WU
 *
 * @param wuId - WU ID to check
 * @param options - Session options
 * @returns True if session exists and matches WU ID
 */
export function hasActiveSessionForWU(wuId: string, options: GetSessionOptions = {}): boolean {
  const session = getCurrentSessionForWU(options);
  return session !== null && session.wu_id === wuId;
}
