/**
 * Chat Session Manager — manages persistent CLI processes per chat session.
 *
 * For CLI adapters (claude, codex, gemini, copilot): keeps a spawnInteractive()
 * process alive. Messages go to stdin, responses come from stdout. The CLI
 * tool manages its own context window natively.
 *
 * For gergy adapter: uses agenticLoop directly with session messages from the
 * session store.
 *
 * On process crash/timeout, context is recovered from the session store.
 */

import { getConfig } from './config.mjs';

// Active chat sessions: sessionId -> ChatSession
const _sessions = new Map();

// Default inactivity timeout before killing a CLI process (10 minutes)
const DEFAULT_INACTIVITY_MS = 10 * 60 * 1000;

class ChatSession {
  /**
   * @param {string} sessionId
   * @param {object} adapter - the loaded LLM adapter module
   */
  constructor(sessionId, adapter) {
    this.sessionId = sessionId;
    this.adapter = adapter;
    this.proc = null;
    this.lastActivity = Date.now();
    this.inactivityTimer = null;
    this.inactivityMs = DEFAULT_INACTIVITY_MS;
    this._buffer = '';
    this._responseQueue = []; // pending response promises
    this._destroyed = false;
  }

  /**
   * Check if this adapter is a CLI adapter (has spawnInteractive).
   */
  get isCLI() {
    return typeof this.adapter.spawnInteractive === 'function' &&
           this.adapter.name !== 'gergy';
  }

  /**
   * Spawn or return existing interactive CLI process.
   */
  ensureProcess() {
    if (this._destroyed) throw new Error('Session destroyed');
    if (this.proc && !this.proc.killed) return this.proc;

    if (!this.isCLI) return null;

    const { spawnInteractive } = this.adapter;
    this.proc = spawnInteractive({
      model: 'balanced'
    });

    this._buffer = '';

    this.proc.stdout.on('data', (chunk) => {
      this._buffer += chunk.toString();
      this._processBuffer();
    });

    this.proc.on('close', () => {
      this.proc = null;
    });

    this.proc.on('error', (err) => {
      console.error(`[chat-session] Process error for ${this.sessionId}: ${err.message}`);
      this.proc = null;
    });

    this._resetInactivityTimer();
    return this.proc;
  }

  /**
   * Process buffered stdout data, resolving pending response promises.
   */
  _processBuffer() {
    const lines = this._buffer.split('\n');
    this._buffer = lines.pop(); // keep incomplete line in buffer

    for (const line of lines) {
      if (!line.trim()) continue;
      try {
        const event = JSON.parse(line);
        if (this._responseQueue.length > 0) {
          this._responseQueue[0].events.push(event);
          if (event.type === 'result' || event.type === 'done') {
            const pending = this._responseQueue.shift();
            pending.resolve(pending.events);
          }
        }
      } catch {
        // Non-JSON line — ignore
      }
    }
  }

  /**
   * Reset the inactivity timer.
   */
  _resetInactivityTimer() {
    if (this.inactivityTimer) clearTimeout(this.inactivityTimer);
    this.inactivityTimer = setTimeout(() => {
      console.log(`[chat-session] Inactivity timeout for ${this.sessionId}, killing process`);
      this._killProcess();
    }, this.inactivityMs);
  }

  /**
   * Kill the underlying CLI process.
   */
  _killProcess() {
    if (this.proc) {
      try { this.proc.kill('SIGTERM'); } catch {}
      this.proc = null;
    }
    if (this.inactivityTimer) {
      clearTimeout(this.inactivityTimer);
      this.inactivityTimer = null;
    }
  }

  /**
   * Send a message and yield response chunks as an async generator.
   * Works for both CLI adapters and gergy.
   *
   * @param {string} message
   * @yields {{ text: string, done: boolean }}
   */
  async *sendMessage(message) {
    if (this._destroyed) throw new Error('Session destroyed');
    this.lastActivity = Date.now();

    if (this.isCLI) {
      yield* this._sendCLI(message);
    } else {
      yield* this._sendGergy(message);
    }
  }

  /**
   * Send message via CLI process stdin, read response from stdout.
   */
  async *_sendCLI(message) {
    const proc = this.ensureProcess();
    if (!proc) throw new Error('Failed to spawn CLI process');

    this._resetInactivityTimer();

    // Write message to stdin
    proc.stdin.write(message + '\n');

    // Collect stream-json events
    let accumulatedText = '';
    let fullResult = '';

    // Create a promise to wait for the response
    const responsePromise = new Promise((resolve) => {
      this._responseQueue.push({ resolve, events: [] });
    });

    // Set a timeout for the response
    const timeout = setTimeout(() => {
      if (this._responseQueue.length > 0) {
        const pending = this._responseQueue.shift();
        pending.resolve(pending.events);
      }
    }, 600_000); // 10 min timeout

    const events = await responsePromise;
    clearTimeout(timeout);

    for (const event of events) {
      if (event.type === 'assistant' && event.message?.content) {
        for (const block of event.message.content) {
          if (block.type === 'text' && block.text) {
            accumulatedText += block.text;
            yield { text: accumulatedText, done: false };
          }
        }
      } else if (event.type === 'result') {
        fullResult = event.result || accumulatedText;
      }
    }

    yield { text: fullResult || accumulatedText, done: true };
  }

  /**
   * Send message via gergy's agenticLoop directly.
   */
  async *_sendGergy(message) {
    // Load session messages for context
    let messages = [];
    try {
      const { getSessionMessages } = await import('./session-store.mjs');
      messages = getSessionMessages(this.sessionId);
    } catch {
      // Session store not available — start fresh
    }

    // Append the new user message
    messages.push({ role: 'user', content: message });

    // Check if adapter exports agenticLoop
    if (typeof this.adapter.agenticLoop === 'function') {
      let accumulatedText = '';
      for await (const event of this.adapter.agenticLoop(messages, { model: 'balanced' })) {
        if (event.type === 'text_delta') {
          accumulatedText += event.text;
          yield { text: accumulatedText, done: false };
        } else if (event.type === 'done') {
          const textParts = (event.contentBlocks || [])
            .filter(b => b.type === 'text')
            .map(b => b.text);
          if (textParts.length > 0) {
            accumulatedText = textParts.join('');
          }
        }
      }
      yield { text: accumulatedText, done: true };
    } else {
      // Fallback: use ask() with serialized context
      const contextPrompt = messages.map(m =>
        `${m.role === 'user' ? 'Human' : 'Assistant'}: ${m.content}`
      ).join('\n\n');

      let accumulatedText = '';
      const result = await this.adapter.ask(contextPrompt, {
        model: 'balanced',
        onStream: (text) => {
          accumulatedText = text;
        }
      });
      yield { text: result || accumulatedText, done: true };
    }
  }

  /**
   * Force respawn the CLI process (after crash or timeout).
   * Reloads context from session store.
   */
  async respawn() {
    this._killProcess();
    if (!this.isCLI) return;

    // Spawn fresh process
    this.ensureProcess();

    // Load conversation history from session store for context recovery
    try {
      const { getSessionMessages } = await import('./session-store.mjs');
      const messages = getSessionMessages(this.sessionId);
      if (messages.length > 0) {
        const history = messages.map(m =>
          `${m.role === 'user' ? 'Human' : 'Assistant'}: ${m.content}`
        ).join('\n\n');
        const recoveryPrompt = `Here is the conversation so far. Continue naturally from where we left off.\n\n${history}`;
        this.proc.stdin.write(recoveryPrompt + '\n');
      }
    } catch {
      // Session store not available — start fresh
    }
  }

  /**
   * Clean up and destroy this session.
   */
  destroy() {
    this._destroyed = true;
    this._killProcess();
    // Reject any pending responses
    for (const pending of this._responseQueue) {
      pending.resolve([]);
    }
    this._responseQueue = [];
  }
}

// ── Module-level session management ──────────────────────────

/**
 * Get or create a ChatSession for the given sessionId.
 * @param {string} sessionId
 * @returns {ChatSession}
 */
export function getOrCreateChatSession(sessionId) {
  if (_sessions.has(sessionId)) {
    return _sessions.get(sessionId);
  }

  // Dynamically get the current adapter
  // We need to lazy-import to avoid circular deps
  let adapter = null;
  try {
    // Adapter is already loaded by llm.mjs — access it indirectly
    const config = getConfig();
    // We'll set the adapter later when it's available
    adapter = _currentAdapter;
  } catch {
    // Adapter not available yet
  }

  if (!adapter) {
    throw new Error('LLM adapter not initialized — call setAdapter() first');
  }

  const session = new ChatSession(sessionId, adapter);
  _sessions.set(sessionId, session);
  return session;
}

/**
 * Destroy a specific chat session.
 * @param {string} sessionId
 */
export function destroyChatSession(sessionId) {
  const session = _sessions.get(sessionId);
  if (session) {
    session.destroy();
    _sessions.delete(sessionId);
  }
}

/**
 * Destroy all active chat sessions.
 */
export function destroyAllSessions() {
  for (const [id, session] of _sessions) {
    session.destroy();
  }
  _sessions.clear();
}

/**
 * Get all active sessions.
 * @returns {Map<string, ChatSession>}
 */
export function getActiveSessions() {
  return new Map(_sessions);
}

// Adapter reference — set by the integration layer (agent.mjs or server.mjs)
let _currentAdapter = null;

/**
 * Set the current LLM adapter for new sessions.
 * Call this after initLLM() completes.
 * @param {object} adapter
 */
export function setAdapter(adapter) {
  _currentAdapter = adapter;
}

export { ChatSession };
