/**
 * SignalManager — Centralized signal.json operations with atomic file handling.
 *
 * Provides robust signal.json management with proper error handling and atomic
 * operations. Replaces scattered signal detection logic throughout the codebase.
 */

import { readFile, unlink, stat } from 'node:fs/promises';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { createModuleLogger } from '../../logger/index.js';

const log = createModuleLogger('signal-manager');

export interface SignalData {
  status: 'done' | 'questions' | 'error';
  questions?: Array<{
    id: string;
    question: string;
    options?: string[];
  }>;
  error?: string;
}

export interface SignalManager {
  clearSignal(agentWorkdir: string): Promise<void>;
  checkSignalExists(agentWorkdir: string): Promise<boolean>;
  readSignal(agentWorkdir: string): Promise<SignalData | null>;
  waitForSignal(agentWorkdir: string, timeoutMs: number): Promise<SignalData | null>;
  validateSignalFile(signalPath: string): Promise<boolean>;
}

export class FileSystemSignalManager implements SignalManager {
  /**
   * Clear signal.json file atomically. Always called before spawn/resume.
   * This prevents race conditions in completion detection.
   */
  async clearSignal(agentWorkdir: string): Promise<void> {
    const signalPath = join(agentWorkdir, '.cw/output/signal.json');
    try {
      await unlink(signalPath);
      log.debug({ agentWorkdir, signalPath }, 'signal.json cleared successfully');
    } catch (error: any) {
      if (error.code !== 'ENOENT') {
        log.warn({ agentWorkdir, signalPath, error: error.message }, 'failed to clear signal.json');
        throw error;
      }
      // File doesn't exist - that's fine, it's already "cleared"
      log.debug({ agentWorkdir, signalPath }, 'signal.json already absent (nothing to clear)');
    }
  }

  /**
   * Check if signal.json file exists synchronously.
   */
  async checkSignalExists(agentWorkdir: string): Promise<boolean> {
    const signalPath = join(agentWorkdir, '.cw/output/signal.json');
    return existsSync(signalPath);
  }

  /**
   * Read and parse signal.json file with robust error handling.
   * Returns null if file doesn't exist or is invalid.
   */
  async readSignal(agentWorkdir: string): Promise<SignalData | null> {
    const signalPath = join(agentWorkdir, '.cw/output/signal.json');

    try {
      if (!existsSync(signalPath)) {
        return null;
      }

      const content = await readFile(signalPath, 'utf-8');
      const trimmed = content.trim();

      if (!trimmed) {
        log.debug({ agentWorkdir, signalPath }, 'signal.json is empty');
        return null;
      }

      const signal = JSON.parse(trimmed) as SignalData;

      // Basic validation
      if (!signal.status || !['done', 'questions', 'error'].includes(signal.status)) {
        log.warn({ agentWorkdir, signalPath, signal }, 'signal.json has invalid status');
        return null;
      }

      log.debug({ agentWorkdir, signalPath, status: signal.status }, 'signal.json read successfully');
      return signal;

    } catch (error) {
      log.warn({
        agentWorkdir,
        signalPath,
        error: error instanceof Error ? error.message : String(error)
      }, 'failed to read or parse signal.json');
      return null;
    }
  }

  /**
   * Wait for signal.json to appear and be valid, with exponential backoff polling.
   * Returns null if timeout is reached or signal is never valid.
   */
  async waitForSignal(agentWorkdir: string, timeoutMs: number): Promise<SignalData | null> {
    const startTime = Date.now();
    const signalPath = join(agentWorkdir, '.cw/output/signal.json');
    let attempt = 0;

    log.debug({ agentWorkdir, timeoutMs }, 'waiting for signal.json to appear');

    while (Date.now() - startTime < timeoutMs) {
      const signal = await this.readSignal(agentWorkdir);
      if (signal) {
        log.debug({
          agentWorkdir,
          signalPath,
          status: signal.status,
          waitTime: Date.now() - startTime
        }, 'signal.json found and valid');
        return signal;
      }

      // Exponential backoff: 100ms, 200ms, 400ms, 800ms, then 1s max
      const delay = Math.min(100 * Math.pow(2, attempt), 1000);
      await new Promise(resolve => setTimeout(resolve, delay));
      attempt++;
    }

    log.debug({
      agentWorkdir,
      signalPath,
      timeoutMs,
      totalWaitTime: Date.now() - startTime
    }, 'timeout waiting for signal.json');
    return null;
  }

  /**
   * Validate that a signal file is complete and properly formatted.
   * Used to detect if file is still being written vs. truly missing/incomplete.
   */
  async validateSignalFile(signalPath: string): Promise<boolean> {
    try {
      if (!existsSync(signalPath)) {
        return false;
      }

      // Check file is not empty and appears complete
      const stats = await stat(signalPath);
      if (stats.size === 0) {
        return false;
      }

      const content = await readFile(signalPath, 'utf-8');
      const trimmed = content.trim();

      if (!trimmed) {
        return false;
      }

      // Check if JSON structure appears complete
      const endsCorrectly = trimmed.endsWith('}') || trimmed.endsWith(']');
      if (!endsCorrectly) {
        return false;
      }

      // Try to parse as JSON to ensure it's valid
      JSON.parse(trimmed);
      return true;

    } catch (error) {
      log.debug({ signalPath, error: error instanceof Error ? error.message : String(error) }, 'signal file validation failed');
      return false;
    }
  }
}