import {
  Injectable,
  Logger,
  HttpException,
  HttpStatus,
  OnModuleDestroy,
  Optional,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import Anthropic from "@anthropic-ai/sdk";
import { randomUUID } from "crypto";
import {
  SpawnAgentRequest,
  SpawnAgentResponse,
  AgentSession,
  AgentType,
} from "./types/agent-spawner.types";
import { AgentIngestionService } from "../agent-ingestion/agent-ingestion.service";

/**
 * Default delay in milliseconds before cleaning up sessions after terminal states
 * This allows time for status queries before the session is removed
 */
const DEFAULT_SESSION_CLEANUP_DELAY_MS = 30000; // 30 seconds
const SUPPORTED_AI_PROVIDERS = ["ollama", "claude", "openai"] as const;
type SupportedAiProvider = (typeof SUPPORTED_AI_PROVIDERS)[number];

/**
 * Service responsible for spawning Claude agents using Anthropic SDK
 */
@Injectable()
export class AgentSpawnerService implements OnModuleDestroy {
  private readonly logger = new Logger(AgentSpawnerService.name);
  private readonly anthropic: Anthropic | undefined;
  private readonly aiProvider: SupportedAiProvider;
  private readonly sessions = new Map<string, AgentSession>();
  private readonly maxConcurrentAgents: number;
  private readonly sessionCleanupDelayMs: number;
  private readonly cleanupTimers = new Map<string, NodeJS.Timeout>();

  constructor(
    private readonly configService: ConfigService,
    @Optional() private readonly agentIngestionService?: AgentIngestionService
  ) {
    const configuredProvider = this.configService.get<string>("orchestrator.aiProvider");
    this.aiProvider = this.normalizeAiProvider(configuredProvider);

    this.logger.log(`AgentSpawnerService resolved AI provider: ${this.aiProvider}`);

    const apiKey = this.configService.get<string>("orchestrator.claude.apiKey");

    if (this.aiProvider === "claude") {
      if (!apiKey) {
        throw new Error("CLAUDE_API_KEY is required when AI_PROVIDER is set to 'claude'");
      }

      this.logger.log("CLAUDE_API_KEY is configured. Initializing Anthropic client.");
      this.anthropic = new Anthropic({ apiKey });
    } else {
      if (apiKey) {
        this.logger.debug(
          `CLAUDE_API_KEY is set but ignored because AI provider is '${this.aiProvider}'`
        );
      } else {
        this.logger.log(`CLAUDE_API_KEY not required for AI provider '${this.aiProvider}'.`);
      }
      this.anthropic = undefined;
    }

    // Default to 20 if not configured
    this.maxConcurrentAgents =
      this.configService.get<number>("orchestrator.spawner.maxConcurrentAgents") ?? 20;

    // Default to 30 seconds if not configured
    this.sessionCleanupDelayMs =
      this.configService.get<number>("orchestrator.spawner.sessionCleanupDelayMs") ??
      DEFAULT_SESSION_CLEANUP_DELAY_MS;

    this.logger.log(
      `AgentSpawnerService initialized with ${this.aiProvider} AI provider (max concurrent agents: ${String(
        this.maxConcurrentAgents
      )}, cleanup delay: ${String(this.sessionCleanupDelayMs)}ms)`
    );
  }

  private normalizeAiProvider(provider?: string): SupportedAiProvider {
    const normalizedProvider = provider?.trim().toLowerCase();

    if (!normalizedProvider) {
      return "ollama";
    }

    if (!SUPPORTED_AI_PROVIDERS.includes(normalizedProvider as SupportedAiProvider)) {
      this.logger.warn(`Unsupported AI provider '${normalizedProvider}'. Defaulting to 'ollama'.`);
      return "ollama";
    }

    return normalizedProvider as SupportedAiProvider;
  }

  /**
   * Clean up all pending cleanup timers on module destroy
   */
  onModuleDestroy(): void {
    this.cleanupTimers.forEach((timer, agentId) => {
      clearTimeout(timer);
      this.logger.debug(`Cleared cleanup timer for agent ${agentId}`);
    });
    this.cleanupTimers.clear();
  }

  private recordSpawnedAgentIngestion(agentId: string, request: SpawnAgentRequest): void {
    if (!this.agentIngestionService) {
      return;
    }

    void this.agentIngestionService
      .recordAgentSpawned(
        agentId,
        request.parentAgentId,
        undefined,
        request.taskId,
        request.agentType
      )
      .catch((error: unknown) => {
        const errorMessage = error instanceof Error ? error.message : String(error);
        this.logger.error(`Failed to record spawned ingestion for ${agentId}: ${errorMessage}`);
      });
  }

  /**
   * Spawn a new agent with the given configuration
   * @param request Agent spawn request
   * @returns Agent spawn response with agentId
   */
  spawnAgent(request: SpawnAgentRequest): SpawnAgentResponse {
    this.logger.log(`Spawning agent for task: ${request.taskId}`);

    // Check concurrent agent limit before proceeding
    this.checkConcurrentAgentLimit();

    // Validate request
    this.validateSpawnRequest(request);

    // Generate unique agent ID
    const agentId = randomUUID();
    const spawnedAt = new Date();

    // Create agent session
    const session: AgentSession = {
      agentId,
      taskId: request.taskId,
      agentType: request.agentType,
      state: "spawning",
      context: request.context,
      options: request.options,
      spawnedAt,
    };

    // Store session
    this.sessions.set(agentId, session);

    this.recordSpawnedAgentIngestion(agentId, request);

    this.logger.log(`Agent spawned successfully: ${agentId} (type: ${request.agentType})`);

    // NOTE: Actual Claude SDK integration will be implemented in next iteration (see issue #TBD)
    // For now, we're just creating the session and tracking it

    return {
      agentId,
      state: "spawning",
      spawnedAt,
    };
  }

  /**
   * Get agent session by agentId
   * @param agentId Unique agent identifier
   * @returns Agent session or undefined if not found
   */
  getAgentSession(agentId: string): AgentSession | undefined {
    return this.sessions.get(agentId);
  }

  /**
   * Find an active session by task ID.
   */
  findAgentSessionByTaskId(taskId: string): AgentSession | undefined {
    return Array.from(this.sessions.values()).find((session) => session.taskId === taskId);
  }

  /**
   * Update in-memory session state for visibility in list/status endpoints.
   */
  setSessionState(
    agentId: string,
    state: AgentSession["state"],
    error?: string,
    completedAt?: Date
  ): void {
    const session = this.sessions.get(agentId);
    if (!session) return;

    session.state = state;
    session.error = error;
    if (completedAt) {
      session.completedAt = completedAt;
    }
    this.sessions.set(agentId, session);
  }

  /**
   * List all agent sessions
   * @returns Array of all agent sessions
   */
  listAgentSessions(): AgentSession[] {
    return Array.from(this.sessions.values());
  }

  /**
   * Remove an agent session from the in-memory map
   * @param agentId Unique agent identifier
   * @returns true if session was removed, false if not found
   */
  removeSession(agentId: string): boolean {
    // Clear any pending cleanup timer for this agent
    const timer = this.cleanupTimers.get(agentId);
    if (timer) {
      clearTimeout(timer);
      this.cleanupTimers.delete(agentId);
    }

    const deleted = this.sessions.delete(agentId);
    if (deleted) {
      this.logger.log(`Session removed for agent ${agentId}`);
    }
    return deleted;
  }

  /**
   * Schedule session cleanup after a delay
   * This allows time for status queries before the session is removed
   * @param agentId Unique agent identifier
   * @param delayMs Optional delay in milliseconds (defaults to configured value)
   */
  scheduleSessionCleanup(agentId: string, delayMs?: number): void {
    const delay = delayMs ?? this.sessionCleanupDelayMs;

    // Clear any existing timer for this agent
    const existingTimer = this.cleanupTimers.get(agentId);
    if (existingTimer) {
      clearTimeout(existingTimer);
    }

    this.logger.debug(`Scheduling session cleanup for agent ${agentId} in ${String(delay)}ms`);

    const timer = setTimeout(() => {
      this.removeSession(agentId);
      this.cleanupTimers.delete(agentId);
    }, delay);

    this.cleanupTimers.set(agentId, timer);
  }

  /**
   * Get the number of pending cleanup timers (for testing)
   * @returns Number of pending cleanup timers
   */
  getPendingCleanupCount(): number {
    return this.cleanupTimers.size;
  }

  /**
   * Check if the concurrent agent limit has been reached
   * @throws HttpException with 429 Too Many Requests if limit reached
   */
  private checkConcurrentAgentLimit(): void {
    const currentCount = this.sessions.size;
    if (currentCount >= this.maxConcurrentAgents) {
      this.logger.warn(
        `Maximum concurrent agents limit reached: ${String(currentCount)}/${String(this.maxConcurrentAgents)}`
      );
      throw new HttpException(
        {
          message: `Maximum concurrent agents limit reached (${String(this.maxConcurrentAgents)}). Please wait for existing agents to complete.`,
          currentCount,
          maxLimit: this.maxConcurrentAgents,
        },
        HttpStatus.TOO_MANY_REQUESTS
      );
    }
  }

  /**
   * Validate spawn agent request
   * @param request Spawn request to validate
   * @throws Error if validation fails
   */
  private validateSpawnRequest(request: SpawnAgentRequest): void {
    if (!request.taskId || request.taskId.trim() === "") {
      throw new Error("taskId is required");
    }

    const validAgentTypes: AgentType[] = ["worker", "reviewer", "tester"];
    if (!validAgentTypes.includes(request.agentType)) {
      throw new Error(`agentType must be one of: ${validAgentTypes.join(", ")}`);
    }

    if (!request.context.repository || request.context.repository.trim() === "") {
      throw new Error("context.repository is required");
    }

    if (!request.context.branch || request.context.branch.trim() === "") {
      throw new Error("context.branch is required");
    }

    if (request.context.workItems.length === 0) {
      throw new Error("context.workItems must not be empty");
    }
  }
}
