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 history + user prompt
    // When history already ends with the current user message (e.g. multimodal with image blocks),
    // don't add a duplicate plain-text user message.
    const history = params.conversationHistory || [];
    const lastHistoryMsg = history.length > 0 ? history[history.length - 1] : null;
    const historyEndsWithUser = lastHistoryMsg?.role === 'user' && Array.isArray(lastHistoryMsg.content);
    const messages: AnthropicMessage[] = historyEndsWithUser
      ? [...history]
      : [...history, { role: 'user', content: params.prompt }];

    let totalInputTokens = 0;
    let totalOutputTokens = 0;
    let turnCount = 0;
    const engineModel = 'claude-sonnet-4-5-20250929';

    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: engineModel,
          max_tokens: 8192,
          messages,
        };

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

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

        // Use streaming API for token-level output
        const stream = client.messages.stream(requestParams, {
          signal: abortController.signal as any,
        });

        const contentBlocks: AnthropicContentBlock[] = [];
        const toolUseBlocks: Array<{ id: string; name: string; input: Record<string, unknown> }> = [];

        // Stream text deltas in real-time
        for await (const event of stream) {
          if (event.type === 'content_block_delta') {
            const delta = (event as any).delta;
            if (delta?.type === 'text_delta' && delta.text) {
              yield { type: 'text' as const, content: delta.text };
            } else if (delta?.type === 'thinking_delta' && delta.thinking) {
              yield { type: 'thinking' as const, content: delta.thinking };
            }
          } else if (event.type === 'content_block_start') {
            const block = (event as any).content_block;
            if (block?.type === 'tool_use') {
              contentBlocks.push(block);
            }
          } else if (event.type === 'content_block_stop') {
            // Tool use blocks are fully accumulated at stop
          }
        }

        // Get final message for tool use and usage
        const response = await stream.finalMessage();

        // Track token usage (input/output separate for billing)
        if (response.usage) {
          totalInputTokens += response.usage.input_tokens ?? 0;
          totalOutputTokens += response.usage.output_tokens ?? 0;
        }

        // Collect tool_use blocks from final response
        for (const block of response.content) {
          if (block.type === 'tool_use') {
            yield {
              type: 'tool_use' as const,
              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) {
          const summaryBlock = response.content.find(
            (b: any) => b.type === 'text' && b.text,
          );
          const summary = (summaryBlock as any)?.text ?? 'Task completed';

          const totalTokens = totalInputTokens + totalOutputTokens;
          // claude-sonnet-4-5: $3/MTok input, $15/MTok output
          const costUsd = (totalInputTokens * 3 + totalOutputTokens * 15) / 1_000_000;
          yield {
            type: 'completed' as const,
            summary,
            tokensUsed: totalTokens,
            inputTokens: totalInputTokens,
            outputTokens: totalOutputTokens,
            costUsd,
            model: engineModel,
          };
          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
      const totalTokens = totalInputTokens + totalOutputTokens;
      const costUsd = (totalInputTokens * 3 + totalOutputTokens * 15) / 1_000_000;
      yield {
        type: 'completed',
        summary: `Agent loop completed after reaching max turns (${params.maxTurns})`,
        tokensUsed: totalTokens,
        inputTokens: totalInputTokens,
        outputTokens: totalOutputTokens,
        costUsd,
        model: engineModel,
      };
    } 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,
    conversationHistory?: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
  ): AsyncGenerator<EngineStreamEvent> {
    yield* this.executeTask({
      sessionId,
      prompt: message,
      systemPrompt: '',
      allowedTools: [],
      maxTurns: 10,
      conversationHistory,
    });
  }

  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: {},
      },
    }));
  }
}
