import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { AgentEnginePort, EngineTaskParams, EngineStreamEvent } from '../../../domain/ports/outbound/agent-engine.port';
import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo';
import { spawn, ChildProcess } from 'child_process';
import * as readline from 'readline';
import { EventEmitter } from 'events';

@Injectable()
export class ClaudeCodeCliEngine implements AgentEnginePort {
  readonly engineType = AgentEngineType.CLAUDE_CODE_CLI;
  private readonly logger = new Logger(ClaudeCodeCliEngine.name);
  private readonly processes = new Map<string, ChildProcess>();

  constructor(private readonly configService: ConfigService) {}

  async *executeTask(params: EngineTaskParams): AsyncGenerator<EngineStreamEvent> {
    // CLI engine does not support multimodal — `-p` only accepts text.
    // If attachments are present, log a warning. Use SDK or API engine for images.
    if (params.conversationHistory?.some(
      (m) => Array.isArray(m.content) && m.content.some((b: any) => b.type === 'image'),
    )) {
      this.logger.warn(
        `[Session ${params.sessionId}] CLI engine does not support multimodal (images). ` +
        `Image content blocks will be ignored. Use claude_agent_sdk or claude_api engine instead.`,
      );
    }

    const args = [
      '-p', params.prompt,
      '--output-format', 'stream-json',
      '--max-turns', String(params.maxTurns),
      '--verbose',
    ];

    if (params.systemPrompt) {
      args.push('--system-prompt', params.systemPrompt);
    }

    if (params.allowedTools && params.allowedTools.length > 0) {
      args.push('--allowedTools', params.allowedTools.join(','));
    }

    if (params.maxBudgetUsd) {
      args.push('--max-budget-usd', String(params.maxBudgetUsd));
    }

    const childProcess = spawn('claude', args, {
      env: { ...process.env },
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    this.processes.set(params.sessionId, childProcess);

    try {
      yield* this.parseProcessStream(childProcess, params.sessionId);
    } finally {
      this.processes.delete(params.sessionId);
    }
  }

  async cancelTask(sessionId: string): Promise<void> {
    const childProcess = this.processes.get(sessionId);
    if (!childProcess) {
      this.logger.warn(`No process found for session ${sessionId}`);
      return;
    }

    // Send SIGTERM for graceful shutdown
    childProcess.kill('SIGTERM');

    // Set a 5-second timeout for SIGKILL fallback
    const killTimeout = setTimeout(() => {
      if (!childProcess.killed) {
        this.logger.warn(`Process for session ${sessionId} did not terminate gracefully, sending SIGKILL`);
        childProcess.kill('SIGKILL');
      }
    }, 5000);

    // Clean up timeout when process exits
    childProcess.once('exit', () => {
      clearTimeout(killTimeout);
    });

    this.processes.delete(sessionId);
  }

  async *continueSession(
    sessionId: string,
    message: string,
    _conversationHistory?: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
  ): AsyncGenerator<EngineStreamEvent> {
    const args = [
      '--session-id', sessionId,
      '--resume',
      '-p', message,
      '--output-format', 'stream-json',
    ];

    const childProcess = spawn('claude', args, {
      env: { ...process.env },
      stdio: ['pipe', 'pipe', 'pipe'],
    });

    this.processes.set(sessionId, childProcess);

    try {
      yield* this.parseProcessStream(childProcess, sessionId);
    } finally {
      this.processes.delete(sessionId);
    }
  }

  async healthCheck(): Promise<boolean> {
    try {
      return new Promise<boolean>((resolve) => {
        const proc = spawn('claude', ['--version'], {
          stdio: ['pipe', 'pipe', 'pipe'],
        });

        proc.on('error', () => resolve(false));
        proc.on('exit', (code) => resolve(code === 0));

        // Timeout after 5 seconds
        setTimeout(() => {
          proc.kill();
          resolve(false);
        }, 5000);
      });
    } catch {
      return false;
    }
  }

  /**
   * Shared helper that reads stdout from a Claude CLI process line by line,
   * parses each JSON line, maps it to EngineStreamEvent, and yields events
   * via an EventEmitter-based async generator pattern.
   */
  private async *parseProcessStream(
    childProcess: ChildProcess,
    sessionId: string,
  ): AsyncGenerator<EngineStreamEvent> {
    const emitter = new EventEmitter();
    const eventQueue: (EngineStreamEvent | null)[] = [];
    let done = false;
    let resolveWait: (() => void) | null = null;

    const pushEvent = (event: EngineStreamEvent | null) => {
      eventQueue.push(event);
      if (resolveWait) {
        const resolve = resolveWait;
        resolveWait = null;
        resolve();
      }
    };

    // Set up readline interface on stdout
    if (childProcess.stdout) {
      const rl = readline.createInterface({
        input: childProcess.stdout,
        crlfDelay: Infinity,
      });

      rl.on('line', (line: string) => {
        const trimmed = line.trim();
        if (!trimmed) return;

        try {
          const parsed = JSON.parse(trimmed);
          const events = this.mapJsonToEvents(parsed);
          for (const event of events) {
            pushEvent(event);
          }
        } catch (err) {
          this.logger.debug(`Non-JSON line from CLI (session ${sessionId}): ${trimmed}`);
        }
      });

      rl.on('close', () => {
        // readline closed, stdout ended
      });
    }

    // Capture stderr for error logging
    if (childProcess.stderr) {
      childProcess.stderr.on('data', (data: Buffer) => {
        const message = data.toString().trim();
        if (message) {
          this.logger.error(`CLI stderr (session ${sessionId}): ${message}`);
        }
      });
    }

    // Handle process exit
    childProcess.on('exit', (code: number | null, signal: string | null) => {
      if (code !== null && code !== 0) {
        pushEvent({
          type: 'error',
          message: `Claude CLI process exited with code ${code}`,
          code: `CLI_EXIT_${code}`,
        });
      }
      done = true;
      pushEvent(null); // Signal end of stream
    });

    childProcess.on('error', (err: Error) => {
      pushEvent({
        type: 'error',
        message: `Failed to spawn Claude CLI: ${err.message}`,
        code: 'CLI_SPAWN_ERROR',
      });
      done = true;
      pushEvent(null);
    });

    // Async generator loop: yield events as they arrive
    while (true) {
      if (eventQueue.length === 0) {
        if (done) break;
        // Wait for next event
        await new Promise<void>((resolve) => {
          resolveWait = resolve;
        });
      }

      while (eventQueue.length > 0) {
        const event = eventQueue.shift();
        if (event == null) {
          return; // End of stream
        }
        yield event;
      }
    }
  }

  /**
   * Maps a parsed JSON object from Claude CLI stream-json output
   * to an array of EngineStreamEvent objects.
   */
  private mapJsonToEvents(parsed: any): EngineStreamEvent[] {
    const events: EngineStreamEvent[] = [];

    if (parsed.type === 'assistant') {
      // Assistant messages contain content blocks
      if (Array.isArray(parsed.message?.content)) {
        for (const block of parsed.message.content) {
          if (block.type === 'text') {
            events.push({ type: 'text', content: block.text });
          } else if (block.type === 'tool_use') {
            events.push({
              type: 'tool_use',
              toolName: block.name,
              input: block.input ?? {},
            });
          }
        }
      } else if (parsed.content_block) {
        // Single content block variant
        const block = parsed.content_block;
        if (block.type === 'text') {
          events.push({ type: 'text', content: block.text });
        } else if (block.type === 'tool_use') {
          events.push({
            type: 'tool_use',
            toolName: block.name,
            input: block.input ?? {},
          });
        }
      }
    } else if (parsed.type === 'tool_result') {
      events.push({
        type: 'tool_result',
        toolName: parsed.tool_name ?? parsed.name ?? 'unknown',
        output: typeof parsed.content === 'string'
          ? parsed.content
          : JSON.stringify(parsed.content ?? ''),
        isError: parsed.is_error ?? false,
      });
    } else if (parsed.type === 'result') {
      const inputTokens = parsed.usage?.input_tokens ?? 0;
      const outputTokens = parsed.usage?.output_tokens ?? 0;
      const totalTokens = inputTokens + outputTokens;
      // Use reported cost if available, otherwise estimate at claude-sonnet-4-5 pricing
      const costUsd = parsed.total_cost_usd != null
        ? Number(parsed.total_cost_usd)
        : (inputTokens * 3 + outputTokens * 15) / 1_000_000;
      events.push({
        type: 'completed',
        summary: parsed.result ?? parsed.text ?? 'Task completed',
        tokensUsed: totalTokens || undefined,
        inputTokens: inputTokens || undefined,
        outputTokens: outputTokens || undefined,
        costUsd: costUsd || undefined,
        model: parsed.model ?? 'claude-sonnet-4-5-20250929',
      });
    } else if (parsed.type === 'error') {
      events.push({
        type: 'error',
        message: parsed.error?.message ?? parsed.message ?? 'Unknown CLI error',
        code: parsed.error?.code ?? 'CLI_ERROR',
      });
    }

    return events;
  }
}
