/**
 * Claude Agent SDK Engine
 *
 * Third engine implementation using @anthropic-ai/claude-agent-sdk.
 * Integrates with the existing EngineRegistry via the AgentEnginePort interface.
 *
 * Key features:
 * - Default subscription auth (operator's Claude Code login, no API key needed)
 * - Optional per-tenant API key billing (AES-256-GCM encrypted)
 * - L2 approval flow via canUseTool callback with configurable auto-approve timeout
 * - SDK session ID persistence for resume support
 * - RBAC-based tool whitelist resolution per tenant/role
 *
 * Billing modes:
 * - 'subscription': Uses inherited CLI auth from `claude login` (default)
 * - 'api_key': Tenant provides own Anthropic API key, passed via env.ANTHROPIC_API_KEY
 */
import { Injectable, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { DataSource } from 'typeorm';
import { AgentEnginePort, EngineTaskParams, EngineStreamEvent } from '../../../domain/ports/outbound/agent-engine.port';
import { AgentEngineType } from '../../../domain/value-objects/agent-engine-type.vo';
import { CommandRiskLevel } from '../../../domain/value-objects/command-risk-level.vo';
import { CommandRiskClassifier } from '../../../domain/services/command-risk-classifier';
import { TenantAgentConfigService } from '../../services/tenant-agent-config.service';
import { AllowedToolsResolverService } from '../../../domain/services/allowed-tools-resolver.service';
import { TenantContextService } from '@it0/common';
import { ApprovalGate } from './approval-gate';
import * as fs from 'fs';
import * as path from 'path';

// Dynamic import helper that survives tsc commonjs compilation
// (tsc converts `await import()` → require() which breaks ESM-only packages)
const dynamicImport = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise<any>;

/** Tracks an active SDK session for cancellation and approval resolution. */
interface ActiveSession {
  abort: AbortController;
  gate: ApprovalGate;
  sdkSessionId?: string;
  /** Whether text was already streamed via stream_event deltas (to avoid duplicate from assistant message). */
  hasStreamedText?: boolean;
}

@Injectable()
export class ClaudeAgentSdkEngine implements AgentEnginePort {
  readonly engineType = AgentEngineType.CLAUDE_AGENT_SDK;
  private readonly logger = new Logger(ClaudeAgentSdkEngine.name);
  private readonly activeSessions = new Map<string, ActiveSession>();
  private readonly classifier = new CommandRiskClassifier();

  constructor(
    private readonly configService: ConfigService,
    private readonly tenantConfigService: TenantAgentConfigService,
    private readonly allowedToolsResolver: AllowedToolsResolverService,
    private readonly dataSource: DataSource,
  ) {}

  async *executeTask(params: EngineTaskParams): AsyncGenerator<EngineStreamEvent> {
    const tenantId = TenantContextService.getTenantId();
    const tenantConfig = await this.tenantConfigService.findByTenantId(tenantId);

    // Build environment — subscription mode uses OAuth from ~/.claude/.credentials.json
    const env: Record<string, string> = { ...process.env } as Record<string, string>;
    // Disable TLS verification for proxy endpoints (self-signed certs)
    const baseURL = this.configService.get<string>('ANTHROPIC_BASE_URL');
    if (baseURL) {
      env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
      env.ANTHROPIC_BASE_URL = baseURL;
    }
    if (tenantConfig?.billingMode === 'api_key') {
      // Tenant uses their own API key
      try {
        env.ANTHROPIC_API_KEY = this.tenantConfigService.decryptApiKey(tenantConfig);
      } catch (err) {
        yield { type: 'error', message: 'Tenant API key not configured or invalid', code: 'API_KEY_ERROR' };
        return;
      }
    } else {
      // Subscription mode: remove API key so SDK uses OAuth credentials
      delete env.ANTHROPIC_API_KEY;
    }

    // Create approval gate with tenant-configurable timeout
    const timeoutSec = tenantConfig?.approvalTimeoutSeconds ?? 120;
    const gate = new ApprovalGate(timeoutSec);
    const abortController = new AbortController();

    this.activeSessions.set(params.sessionId, { abort: abortController, gate });

    // Event queue for merging SDK stream events and approval events
    const eventQueue: (EngineStreamEvent | null)[] = [];
    let resolveWait: (() => void) | null = null;

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

    try {
      const { query } = await dynamicImport('@anthropic-ai/claude-agent-sdk');

      // Set tenant-isolated HOME directory so SDK session files are separated per tenant
      this.ensureTenantHome(tenantId, env);

      // Load tenant skills and build enriched system prompt
      const skills = await this.loadTenantSkills(tenantId);
      const enrichedSystemPrompt = this.buildSystemPromptWithSkills(params.systemPrompt, skills);

      // Build SDK query options
      const sdkOptions: Record<string, any> = {
        systemPrompt: enrichedSystemPrompt,
        allowedTools: params.allowedTools?.length ? params.allowedTools : undefined,
        maxTurns: params.maxTurns,
        maxBudgetUsd: params.maxBudgetUsd,
        env,
        abortController,
        includePartialMessages: true,
        allowDangerouslySkipPermissions: true,
        permissionMode: 'bypassPermissions',
        stderr: (data: string) => {
          this.logger.debug(`SDK stderr (${params.sessionId}): ${data.trim()}`);
        },
        canUseTool: async (toolName: string, toolInput: any, { signal }: { signal: AbortSignal }) => {
          const riskLevel = this.classifyToolRisk(toolName, toolInput);

          // L0-L1: auto-approve
          if (riskLevel <= CommandRiskLevel.LOW_RISK_WRITE) {
            return { behavior: 'allow' as const, updatedInput: toolInput };
          }

          // L3: always block
          if (riskLevel >= CommandRiskLevel.FORBIDDEN) {
            return {
              behavior: 'deny' as const,
              message: `Command blocked: risk level FORBIDDEN`,
              interrupt: false,
            };
          }

          // L2: emit approval_required and wait for gate
          pushEvent({
            type: 'approval_required',
            command: `${toolName}: ${JSON.stringify(toolInput).slice(0, 200)}`,
            riskLevel: riskLevel,
            taskId: params.sessionId,
          });

          const approved = await gate.waitForApproval(toolName, toolInput as Record<string, unknown>);

          if (approved) {
            return { behavior: 'allow' as const, updatedInput: toolInput };
          } else {
            return {
              behavior: 'deny' as const,
              message: 'User rejected the command',
              interrupt: false,
            };
          }
        },
      };

      // Native SDK resume: if we have a previous SDK session ID, resume it
      if (params.resumeSessionId) {
        sdkOptions.resume = params.resumeSessionId;
        this.logger.log(`Resuming SDK session: ${params.resumeSessionId} for session ${params.sessionId}`);
      }

      // Build prompt: use multimodal SDKUserMessage when attachments are present
      const sdkPrompt = this.buildSdkPrompt(params);

      const sdkQuery = query({
        prompt: sdkPrompt,
        options: sdkOptions,
      });

      // Consume SDK messages in the background and push to event queue
      (async () => {
        try {
          for await (const message of sdkQuery) {
            // Capture SDK session ID for resume support
            if ('session_id' in message && message.session_id) {
              const session = this.activeSessions.get(params.sessionId);
              if (session) {
                session.sdkSessionId = message.session_id;
              }
            }

            const events = this.mapSdkMessage(message, params.sessionId);
            for (const event of events) {
              pushEvent(event);
            }
          }
        } catch (err: any) {
          if (err?.name !== 'AbortError') {
            pushEvent({
              type: 'error',
              message: err?.message ?? 'SDK execution error',
              code: 'SDK_ERROR',
            });
          }
        } finally {
          pushEvent(null); // Signal end of stream
        }
      })();

      // Yield events from the merged queue
      while (true) {
        if (eventQueue.length === 0) {
          await new Promise<void>((resolve) => {
            resolveWait = resolve;
          });
        }

        while (eventQueue.length > 0) {
          const event = eventQueue.shift();
          if (event === null || event === undefined) return;
          yield event;
        }
      }
    } catch (err: any) {
      yield {
        type: 'error',
        message: `Failed to initialize Agent SDK: ${err?.message}`,
        code: 'SDK_INIT_ERROR',
      };
    } finally {
      const session = this.activeSessions.get(params.sessionId);
      if (session) {
        session.gate.dispose();
      }
      this.activeSessions.delete(params.sessionId);
    }
  }

  async cancelTask(sessionId: string): Promise<void> {
    const session = this.activeSessions.get(sessionId);
    if (session) {
      session.gate.dispose();
      session.abort.abort();
      this.activeSessions.delete(sessionId);
    }
  }

  async *continueSession(
    sessionId: string,
    message: string,
    conversationHistory?: Array<{ role: 'user' | 'assistant'; content: string | any[] }>,
  ): AsyncGenerator<EngineStreamEvent> {
    const session = this.activeSessions.get(sessionId);

    // If there's a pending approval, resolve it
    if (session?.gate.hasPending()) {
      session.gate.resolveApproval(message === 'approved');
      return;
    }

    // Otherwise, resume the SDK session
    const sdkSessionId = session?.sdkSessionId;
    if (!sdkSessionId) {
      yield {
        type: 'error',
        message: `No SDK session found for session ${sessionId}`,
        code: 'SESSION_NOT_FOUND',
      };
      return;
    }

    const tenantId = TenantContextService.getTenantId();
    const tenantConfig = await this.tenantConfigService.findByTenantId(tenantId);

    const env: Record<string, string> = { ...process.env } as Record<string, string>;
    const baseURL = this.configService.get<string>('ANTHROPIC_BASE_URL');
    if (baseURL) {
      env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
      env.ANTHROPIC_BASE_URL = baseURL;
    }
    if (tenantConfig?.billingMode === 'api_key') {
      try {
        env.ANTHROPIC_API_KEY = this.tenantConfigService.decryptApiKey(tenantConfig);
      } catch {
        yield { type: 'error', message: 'Tenant API key invalid', code: 'API_KEY_ERROR' };
        return;
      }
    } else {
      delete env.ANTHROPIC_API_KEY;
    }

    // Set tenant-isolated HOME directory
    this.ensureTenantHome(tenantId, env);

    const timeoutSec = tenantConfig?.approvalTimeoutSeconds ?? 120;
    const gate = new ApprovalGate(timeoutSec);
    const abortController = new AbortController();

    this.activeSessions.set(sessionId, { abort: abortController, gate, sdkSessionId });

    try {
      const { query } = await dynamicImport('@anthropic-ai/claude-agent-sdk');

      const sdkQuery = query({
        prompt: message,
        options: {
          resume: sdkSessionId,
          includePartialMessages: true,
          env,
          abortController,
          allowDangerouslySkipPermissions: true,
          permissionMode: 'bypassPermissions',
          canUseTool: async (toolName: string, toolInput: any) => {
            const riskLevel = this.classifyToolRisk(toolName, toolInput);
            if (riskLevel <= CommandRiskLevel.LOW_RISK_WRITE) {
              return { behavior: 'allow' as const, updatedInput: toolInput };
            }
            if (riskLevel >= CommandRiskLevel.FORBIDDEN) {
              return { behavior: 'deny' as const, message: 'Command blocked: FORBIDDEN', interrupt: false };
            }
            const approved = await gate.waitForApproval(toolName, toolInput as Record<string, unknown>);
            return approved
              ? { behavior: 'allow' as const, updatedInput: toolInput }
              : { behavior: 'deny' as const, message: 'User rejected', interrupt: false };
          },
        },
      });

      for await (const msg of sdkQuery) {
        const events = this.mapSdkMessage(msg, sessionId);
        for (const event of events) {
          yield event;
        }
      }
    } catch (err: any) {
      if (err?.name !== 'AbortError') {
        yield { type: 'error', message: err?.message ?? 'SDK resume error', code: 'SDK_ERROR' };
      }
    } finally {
      const s = this.activeSessions.get(sessionId);
      if (s) s.gate.dispose();
      this.activeSessions.delete(sessionId);
    }
  }

  async healthCheck(): Promise<boolean> {
    try {
      await dynamicImport('@anthropic-ai/claude-agent-sdk');
      return true;
    } catch {
      return false;
    }
  }

  /**
   * Resolve the approval for a pending session (called from controller).
   */
  resolveApproval(sessionId: string, approved: boolean): boolean {
    const session = this.activeSessions.get(sessionId);
    if (!session?.gate.hasPending()) return false;
    session.gate.resolveApproval(approved);
    return true;
  }

  /**
   * Get the SDK session ID for persistence in AgentSession.metadata.
   */
  getSdkSessionId(sessionId: string): string | undefined {
    return this.activeSessions.get(sessionId)?.sdkSessionId;
  }

  /**
   * Load active skills from the tenant's schema `skills` table.
   * Skills are injected into the system prompt so the agent knows what it can do.
   */
  private async loadTenantSkills(tenantId: string): Promise<Array<{ name: string; content: string }>> {
    const safeId = tenantId.replace(/[^a-zA-Z0-9_]/g, '');
    const schemaName = `it0_t_${safeId}`;
    try {
      const skills: Array<{ name: string; content: string }> = await this.dataSource.query(
        `SELECT name, content FROM "${schemaName}".skills WHERE is_active = true ORDER BY name`,
      );
      this.logger.log(`Loaded ${skills.length} skills for tenant ${tenantId}`);
      return skills;
    } catch (err: any) {
      this.logger.warn(`Failed to load skills for tenant ${tenantId}: ${err.message}`);
      return [];
    }
  }

  /**
   * Build enriched system prompt by appending tenant skills.
   */
  private buildSystemPromptWithSkills(
    basePrompt: string | undefined,
    skills: Array<{ name: string; content: string }>,
  ): string | undefined {
    if (skills.length === 0) return basePrompt || undefined;

    const skillsSection = skills
      .map((s) => `### ${s.name}\n${s.content}`)
      .join('\n\n');

    const enrichment = `\n\n## Available Skills\n\nThe following skills are configured for this tenant. Use them as reference knowledge when executing tasks.\n\n${skillsSection}`;

    return basePrompt ? `${basePrompt}${enrichment}` : enrichment.trim();
  }

  /**
   * Set up a tenant-isolated HOME directory so SDK session files
   * are stored separately per tenant. Creates the directory structure
   * and symlinks shared credentials if needed.
   */
  private ensureTenantHome(tenantId: string, env: Record<string, string>): void {
    const basePath = '/data/claude-tenants';
    const tenantHome = path.join(basePath, tenantId);
    const tenantClaudeDir = path.join(tenantHome, '.claude');

    // Create tenant directory structure if it doesn't exist
    if (!fs.existsSync(tenantClaudeDir)) {
      fs.mkdirSync(tenantClaudeDir, { recursive: true });
      this.logger.log(`Created tenant HOME directory: ${tenantHome}`);
    }

    // Symlink shared credentials for subscription mode
    const credentialsTarget = path.join(tenantClaudeDir, '.credentials.json');
    const sharedCredentials = '/home/appuser/.claude/.credentials.json';
    if (!fs.existsSync(credentialsTarget) && fs.existsSync(sharedCredentials)) {
      try {
        fs.symlinkSync(sharedCredentials, credentialsTarget);
        this.logger.log(`Symlinked credentials for tenant ${tenantId}`);
      } catch (err: any) {
        this.logger.warn(`Failed to symlink credentials for tenant ${tenantId}: ${err.message}`);
        // Fallback: copy the file instead
        try {
          fs.copyFileSync(sharedCredentials, credentialsTarget);
          this.logger.log(`Copied credentials for tenant ${tenantId} (symlink failed)`);
        } catch (copyErr: any) {
          this.logger.error(`Failed to copy credentials for tenant ${tenantId}: ${copyErr.message}`);
        }
      }
    }

    // Also symlink .claude.json (settings file) if it exists
    const settingsTarget = path.join(tenantHome, '.claude.json');
    const sharedSettings = '/home/appuser/.claude.json';
    if (!fs.existsSync(settingsTarget) && fs.existsSync(sharedSettings)) {
      try {
        fs.symlinkSync(sharedSettings, settingsTarget);
      } catch {
        try { fs.copyFileSync(sharedSettings, settingsTarget); } catch { /* ignore */ }
      }
    }

    // Set HOME to tenant-specific directory
    env.HOME = tenantHome;
    this.logger.debug(`Set HOME=${tenantHome} for tenant ${tenantId}`);
  }

  /**
   * Build the SDK prompt: plain string for text-only, or AsyncIterable<SDKUserMessage>
   * for multimodal (images). Checks if the last history message contains image blocks.
   */
  private buildSdkPrompt(
    params: EngineTaskParams,
  ): string | AsyncIterable<any> {
    const history = params.conversationHistory;
    if (!history || history.length === 0) {
      return params.prompt;
    }

    // Check if the last message in history is a user message with multimodal content blocks
    const lastMsg = history[history.length - 1];
    const hasMultimodalBlocks =
      lastMsg.role === 'user' &&
      Array.isArray(lastMsg.content) &&
      lastMsg.content.some(
        (block: any) => block.type === 'image' || block.type === 'document',
      );

    if (!hasMultimodalBlocks) {
      return params.prompt;
    }

    // Build a single SDKUserMessage with multimodal content (images + documents)
    this.logger.log(`Building multimodal SDK prompt with content blocks`);

    const userMessage = {
      type: 'user' as const,
      message: {
        role: 'user' as const,
        content: lastMsg.content,
      },
      parent_tool_use_id: null,
      session_id: '',
    };

    // Return an async iterable that yields a single SDKUserMessage
    async function* singleMessage() {
      yield userMessage;
    }

    return singleMessage();
  }

  private classifyToolRisk(toolName: string, toolInput: any): CommandRiskLevel {
    // Only classify Bash commands for risk; other tools are auto-allowed
    if (toolName === 'Bash' && typeof toolInput?.command === 'string') {
      return this.classifier.classify(toolInput.command);
    }
    // Write/Edit operations are low-risk write
    if (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') {
      return CommandRiskLevel.LOW_RISK_WRITE;
    }
    // Read-only tools
    return CommandRiskLevel.READ_ONLY;
  }

  private mapSdkMessage(message: any, sessionId: string): EngineStreamEvent[] {
    const events: EngineStreamEvent[] = [];
    const session = this.activeSessions.get(sessionId);

    // Log every SDK message type for debugging
    if (message.type === 'stream_event') {
      const se = message.event;
      this.logger.debug(`SDK msg: stream_event → ${se?.type} / ${se?.delta?.type ?? 'no-delta'}`);
    } else {
      this.logger.log(`SDK msg: type=${message.type}${message.subtype ? ` subtype=${message.subtype}` : ''}`);
    }

    if (message.type === 'stream_event') {
      // Token-level streaming from includePartialMessages: true
      const streamEvent = message.event;
      if (!streamEvent) return events;

      if (streamEvent.type === 'content_block_delta') {
        const delta = streamEvent.delta;
        if (delta?.type === 'text_delta' && delta.text) {
          events.push({ type: 'text', content: delta.text });
          if (session) session.hasStreamedText = true;
        } else if (delta?.type === 'thinking_delta' && delta.thinking) {
          events.push({ type: 'thinking', content: delta.thinking });
          if (session) session.hasStreamedText = true;
        }
      }
      // Ignore content_block_start/stop/message_start/message_stop — they are structural
    } else if (message.type === 'assistant') {
      // Complete assistant message — extract content blocks.
      // Skip text/thinking if already streamed via stream_event deltas to avoid duplication.
      const alreadyStreamed = session?.hasStreamedText ?? false;
      // Reset flag for next assistant turn
      if (session) session.hasStreamedText = false;

      const content = message.message?.content;
      if (Array.isArray(content)) {
        for (const block of content) {
          if (block.type === 'text' && block.text && !alreadyStreamed) {
            events.push({ type: 'text', content: block.text });
          } else if (block.type === 'thinking' && block.thinking && !alreadyStreamed) {
            events.push({ type: 'thinking', content: block.thinking });
          } else if (block.type === 'tool_use') {
            events.push({
              type: 'tool_use',
              toolName: block.name ?? 'unknown',
              input: block.input ?? {},
            });
          } else if (block.type === 'tool_result') {
            events.push({
              type: 'tool_result',
              toolName: block.tool_use_id ?? 'unknown',
              output: typeof block.content === 'string'
                ? block.content
                : JSON.stringify(block.content ?? ''),
              isError: block.is_error ?? false,
            });
          }
        }
      }
    } else if (message.type === 'result') {
      const inputTokens = message.usage?.input_tokens ?? 0;
      const outputTokens = message.usage?.output_tokens ?? 0;
      const totalTokens = inputTokens + outputTokens;
      const costUsd = message.total_cost_usd != null
        ? Number(message.total_cost_usd)
        : (inputTokens * 3 + outputTokens * 15) / 1_000_000;
      events.push({
        type: 'completed',
        summary: message.result ?? '',
        tokensUsed: totalTokens || undefined,
        inputTokens: inputTokens || undefined,
        outputTokens: outputTokens || undefined,
        costUsd: costUsd || undefined,
        model: message.model ?? 'claude-sonnet-4-5-20250929',
      });
    } else if (message.type === 'system' && message.subtype === 'init') {
      this.logger.log(`SDK session initialized: ${message.session_id}`);
    }

    return events;
  }
}
