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 { ToolExecutor } from './tool-executor';
import Anthropic from '@anthropic-ai/sdk';

interface AnthropicMessage {
  role: 'user' | 'assistant';
  content: string | AnthropicContentBlock[];
}

interface AnthropicContentBlock {
  type: string;
  text?: string;
  id?: string;
  name?: string;
  input?: Record<string, unknown>;
}

interface AnthropicToolResultBlock {
  type: 'tool_result';
  tool_use_id: string;
  content: string;
}

@Injectable()
export class ClaudeApiEngine implements AgentEnginePort {
  readonly engineType = AgentEngineType.CLAUDE_API;
  private readonly logger = new Logger(ClaudeApiEngine.name);
  private readonly abortControllers = new Map<string, AbortController>();

  constructor(
    private readonly configService: ConfigService,
    private readonly toolExecutor: ToolExecutor,
  ) {}

  async *executeTask(params: EngineTaskParams): AsyncGenerator<EngineStreamEvent> {
    const apiKey = this.configService.get<string>('ANTHROPIC_API_KEY');
    if (!apiKey) {
      yield {
        type: 'error',
        message: 'ANTHROPIC_API_KEY is not configured',
        code: 'API_KEY_MISSING',
      };
      return;
    }

    const baseURL = this.configService.get<string>('ANTHROPIC_BASE_URL');
    if (baseURL) {
      process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
    }
    const client = new Anthropic({ apiKey, ...(baseURL ? { baseURL } : {}) });

    const abortController = new AbortController();
    this.abortControllers.set(params.sessionId, abortController);

    // Build tools array from allowedTools
    const tools = this.buildToolDefinitions(params.allowedTools);

    // Initialize conversation with user prompt
    const messages: AnthropicMessage[] = [
      { role: 'user', content: params.prompt },
    ];

    let totalTokensUsed = 0;
    let turnCount = 0;

    try {
      // Agent loop: continue until end_turn or max turns reached
      while (turnCount < params.maxTurns) {
        turnCount++;

        // Check for abort before each API call
        if (abortController.signal.aborted) {
          yield {
            type: 'error',
            message: 'Task was cancelled',
            code: 'TASK_CANCELLED',
          };
          return;
        }

        const requestParams: any = {
          model: 'claude-sonnet-4-5-20250929',
          max_tokens: 8192,
          messages,
        };

        if (params.systemPrompt) {
          requestParams.system = params.systemPrompt;
        }

        if (tools.length > 0) {
          requestParams.tools = tools;
        }

        const response = await client.messages.create(requestParams, {
          signal: abortController.signal as any,
        });

        // Track token usage
        if (response.usage) {
          totalTokensUsed += (response.usage.input_tokens ?? 0) + (response.usage.output_tokens ?? 0);
        }

        // Process content blocks from the response
        const contentBlocks = response.content as AnthropicContentBlock[];
        const toolUseBlocks: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];

        for (const block of contentBlocks) {
          if (block.type === 'text' && block.text) {
            yield { type: 'text', content: block.text };
          } else if (block.type === 'tool_use') {
            yield {
              type: 'tool_use',
              toolName: block.name!,
              input: (block.input as Record<string, unknown>) ?? {},
            };
            toolUseBlocks.push({
              id: block.id!,
              name: block.name!,
              input: (block.input as Record<string, unknown>) ?? {},
            });
          }
        }

        // Check stop reason
        if (response.stop_reason === 'end_turn' || toolUseBlocks.length === 0) {
          // Extract final text as summary
          const summaryBlock = contentBlocks.find(
            (b) => b.type === 'text' && b.text,
          );
          const summary = summaryBlock?.text ?? 'Task completed';

          yield {
            type: 'completed',
            summary,
            tokensUsed: totalTokensUsed,
          };
          return;
        }

        // If there are tool uses, we need to continue the loop
        // Push assistant message with all content blocks
        messages.push({
          role: 'assistant',
          content: contentBlocks,
        });

        // Execute tools via ToolExecutor and collect results
        const toolResults: AnthropicToolResultBlock[] = [];
        for (const toolUse of toolUseBlocks) {
          const result = await this.toolExecutor.execute(
            toolUse.name,
            toolUse.input as Record<string, any>,
          );

          toolResults.push({
            type: 'tool_result' as const,
            tool_use_id: toolUse.id,
            content: result.output,
          });

          yield {
            type: 'tool_result',
            toolName: toolUse.name,
            output: result.output,
            isError: result.isError,
          };
        }

        // Add tool results as user message to continue the conversation
        messages.push({
          role: 'user',
          content: toolResults,
        });
      }

      // Max turns reached
      yield {
        type: 'completed',
        summary: `Agent loop completed after reaching max turns (${params.maxTurns})`,
        tokensUsed: totalTokensUsed,
      };
    } catch (error: any) {
      if (error.name === 'AbortError' || abortController.signal.aborted) {
        yield {
          type: 'error',
          message: 'Task was cancelled',
          code: 'TASK_CANCELLED',
        };
      } else {
        this.logger.error(`Claude API error (session ${params.sessionId}): ${error.message}`);
        yield {
          type: 'error',
          message: error.message ?? 'Unknown API error',
          code: error.status ? `API_ERROR_${error.status}` : 'API_ERROR',
        };
      }
    } finally {
      this.abortControllers.delete(params.sessionId);
    }
  }

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

    controller.abort();
    this.abortControllers.delete(sessionId);
  }

  async *continueSession(sessionId: string, message: string): AsyncGenerator<EngineStreamEvent> {
    // For the API engine, continuing a session requires the full message history
    // which should be managed by the orchestrator layer. Here we treat it as a
    // new single-turn interaction for simplicity.
    yield* this.executeTask({
      sessionId,
      prompt: message,
      systemPrompt: '',
      allowedTools: [],
      maxTurns: 1,
    });
  }

  async healthCheck(): Promise<boolean> {
    try {
      const apiKey = this.configService.get<string>('ANTHROPIC_API_KEY');
      if (!apiKey) return false;

      const baseURL = this.configService.get<string>('ANTHROPIC_BASE_URL');
      if (baseURL) {
        process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
      }
      const client = new Anthropic({ apiKey, ...(baseURL ? { baseURL } : {}) });

      // Make a minimal API call to verify connectivity
      const response = await client.messages.create({
        model: 'claude-sonnet-4-5-20250929',
        max_tokens: 10,
        messages: [{ role: 'user', content: 'ping' }],
      });

      return !!response.id;
    } catch {
      return false;
    }
  }

  /**
   * Builds Anthropic tool definitions from an array of tool name strings.
   * Maps known tool names to their schemas; unknown tools get a generic definition.
   */
  private buildToolDefinitions(
    allowedTools: string[],
  ): Array<{ name: string; description: string; input_schema: Record<string, unknown> }> {
    if (!allowedTools || allowedTools.length === 0) return [];

    return allowedTools.map((toolName) => ({
      name: toolName,
      description: `Tool: ${toolName}`,
      input_schema: {
        type: 'object' as const,
        properties: {},
      },
    }));
  }
}
