/**
 * Bulk SEO Operations
 *
 * This module provides batch processing capabilities for SEO operations,
 * including metadata updates, content analysis, and schema generation.
 * It supports chunked processing, progress tracking, retry logic, and dry-run mode.
 *
 * @since 2.7.0
 */

import { WordPressClient } from "@/client/api.js";
import { MetaGenerator } from "./generators/MetaGenerator.js";
import { ContentAnalyzer } from "./analyzers/ContentAnalyzer.js";
import { SEOCacheManager } from "@/cache/SEOCacheManager.js";
import { LoggerFactory } from "@/utils/logger.js";
import { SEOToolParams, BulkOperationResult, SEOAnalysisResult } from "@/types/seo.js";
import type { WordPressPost } from "@/types/wordpress.js";

/**
 * Configuration for bulk operations
 */
interface BulkOperationConfig {
  /** Number of items per batch */
  batchSize: number;

  /** Maximum number of retry attempts */
  maxRetries: number;

  /** Initial delay for exponential backoff (ms) */
  retryDelayMs: number;

  /** Maximum delay for exponential backoff (ms) */
  maxRetryDelayMs: number;

  /** Timeout per operation (ms) */
  operationTimeoutMs: number;

  /** Enable progress callbacks */
  enableProgress: boolean;
}

/**
 * Progress information for bulk operations
 */
interface BulkProgress {
  /** Total items to process */
  total: number;

  /** Items processed so far */
  processed: number;

  /** Items successfully completed */
  completed: number;

  /** Items that failed */
  failed: number;

  /** Items that were skipped */
  skipped: number;

  /** Current batch being processed */
  currentBatch: number;

  /** Total number of batches */
  totalBatches: number;

  /** Estimated completion time */
  eta?: Date;

  /** Average processing time per item (ms) */
  avgProcessingTime: number;
}

/**
 * Error information for failed operations
 */
interface BulkOperationError {
  /** Post ID that failed */
  postId: number;

  /** Error message */
  error: string;

  /** Retry attempt count */
  attempts: number;

  /** Whether this error is retryable */
  retryable: boolean;
}

/**
 * Type for progress callback function
 */
type ProgressCallback = (progress: BulkProgress) => void;

/**
 * Bulk SEO Operations Manager
 */
export class BulkOperations {
  private logger = LoggerFactory.tool("bulk_operations");
  private config: BulkOperationConfig;
  private metaGenerator: MetaGenerator;
  private contentAnalyzer: ContentAnalyzer;
  private cacheManager: SEOCacheManager | undefined;

  constructor(
    private client: WordPressClient,
    cacheManager?: SEOCacheManager,
    config?: Partial<BulkOperationConfig>,
  ) {
    this.cacheManager = cacheManager;
    this.metaGenerator = new MetaGenerator();
    this.contentAnalyzer = new ContentAnalyzer();

    // Default configuration
    this.config = {
      batchSize: 10,
      maxRetries: 3,
      retryDelayMs: 1000,
      maxRetryDelayMs: 30000,
      operationTimeoutMs: 60000,
      enableProgress: true,
      ...config,
    };
  }

  /**
   * Bulk update metadata for multiple posts
   */
  async bulkUpdateMetadata(params: SEOToolParams, progressCallback?: ProgressCallback): Promise<BulkOperationResult> {
    const startTime = Date.now();
    this.logger.info("Starting bulk metadata update", {
      postIds: params.postIds?.length,
      dryRun: params.dryRun,
      batchSize: this.config.batchSize,
    });

    if (!params.postIds?.length) {
      throw new Error("No post IDs provided for bulk operation");
    }

    const progress: BulkProgress = {
      total: params.postIds.length,
      processed: 0,
      completed: 0,
      failed: 0,
      skipped: 0,
      currentBatch: 0,
      totalBatches: Math.ceil(params.postIds.length / this.config.batchSize),
      avgProcessingTime: 0,
    };

    const errors: BulkOperationError[] = [];
    const batches = this.createBatches(params.postIds, this.config.batchSize);

    // Process each batch
    for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
      const batch = batches[batchIndex];
      progress.currentBatch = batchIndex + 1;

      this.logger.debug(`Processing batch ${progress.currentBatch}/${progress.totalBatches}`, {
        batchSize: batch.length,
        postIds: batch,
      });

      // Process batch items
      await Promise.all(
        batch.map(async (postId) => {
          const itemStartTime = Date.now();

          try {
            await this.processMetadataUpdate(postId, params);
            progress.completed++;

            // Update average processing time
            const processingTime = Date.now() - itemStartTime;
            progress.avgProcessingTime =
              (progress.avgProcessingTime * progress.processed + processingTime) / (progress.processed + 1);
          } catch (_error) {
            const bulkError: BulkOperationError = {
              postId,
              error: _error instanceof Error ? _error.message : String(_error),
              attempts: 1,
              retryable: this.isRetryableError(_error),
            };

            // Attempt retries
            if (bulkError.retryable) {
              const retryResult = await this.retryOperation(
                () => this.processMetadataUpdate(postId, params),
                bulkError,
              );

              if (retryResult.success) {
                progress.completed++;
              } else {
                progress.failed++;
                errors.push(retryResult.error);
              }
            } else {
              progress.failed++;
              errors.push(bulkError);
            }
          }

          progress.processed++;
        }),
      );

      // Calculate ETA
      if (progress.avgProcessingTime > 0 && progress.processed < progress.total) {
        const remainingItems = progress.total - progress.processed;
        const etaMs = remainingItems * progress.avgProcessingTime;
        progress.eta = new Date(Date.now() + etaMs);
      } else if (progress.processed > 0 && progress.processed < progress.total) {
        // Fallback ETA calculation even with minimal processing time
        const remainingItems = progress.total - progress.processed;
        const averageTime = progress.avgProcessingTime || 100; // Fallback to 100ms
        const etaMs = remainingItems * averageTime;
        progress.eta = new Date(Date.now() + etaMs);
      }

      // Call progress callback
      if (progressCallback && this.config.enableProgress) {
        progressCallback(progress);
      }

      // Small delay between batches to avoid overwhelming the server
      if (batchIndex < batches.length - 1) {
        await this.delay(100);
      }
    }

    const result: BulkOperationResult = {
      total: params.postIds.length,
      success: progress.completed,
      failed: progress.failed,
      skipped: progress.skipped,
      errors: errors.map((e) => ({ postId: e.postId, error: e.error })),
      processingTime: Date.now() - startTime,
      dryRun: params.dryRun || false,
    };

    this.logger.info("Bulk metadata update completed", {
      ...result,
      successRate: ((result.success / result.total) * 100).toFixed(1) + "%",
    });

    return result;
  }

  /**
   * Bulk analyze content for multiple posts
   */
  async bulkAnalyzeContent(
    params: SEOToolParams,
    progressCallback?: ProgressCallback,
  ): Promise<{ results: SEOAnalysisResult[]; summary: BulkOperationResult }> {
    const startTime = Date.now();
    this.logger.info("Starting bulk content analysis", {
      postIds: params.postIds?.length,
      analysisType: params.analysisType,
      batchSize: this.config.batchSize,
    });

    if (!params.postIds?.length) {
      throw new Error("No post IDs provided for bulk analysis");
    }

    const results: SEOAnalysisResult[] = [];
    const progress: BulkProgress = {
      total: params.postIds.length,
      processed: 0,
      completed: 0,
      failed: 0,
      skipped: 0,
      currentBatch: 0,
      totalBatches: Math.ceil(params.postIds.length / this.config.batchSize),
      avgProcessingTime: 0,
    };

    const errors: BulkOperationError[] = [];
    const batches = this.createBatches(params.postIds, this.config.batchSize);

    // Process each batch
    for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
      const batch = batches[batchIndex];
      progress.currentBatch = batchIndex + 1;

      // Process batch items
      await Promise.all(
        batch.map(async (postId) => {
          const itemStartTime = Date.now();

          try {
            const analysisResult = await this.processContentAnalysis(postId, params);
            results.push(analysisResult);
            progress.completed++;

            // Update average processing time
            const processingTime = Date.now() - itemStartTime;
            progress.avgProcessingTime =
              (progress.avgProcessingTime * progress.processed + processingTime) / (progress.processed + 1);
          } catch (_error) {
            const bulkError: BulkOperationError = {
              postId,
              error: _error instanceof Error ? _error.message : String(_error),
              attempts: 1,
              retryable: this.isRetryableError(_error),
            };

            // Attempt retries for analysis
            if (bulkError.retryable) {
              const retryResult = await this.retryOperation(
                () => this.processContentAnalysis(postId, params),
                bulkError,
              );

              if (retryResult.success) {
                results.push(retryResult.result!);
                progress.completed++;
              } else {
                progress.failed++;
                errors.push(retryResult.error);
              }
            } else {
              progress.failed++;
              errors.push(bulkError);
            }
          }

          progress.processed++;
        }),
      );

      // Call progress callback
      if (progressCallback && this.config.enableProgress) {
        progressCallback(progress);
      }
    }

    const summary: BulkOperationResult = {
      total: params.postIds.length,
      success: progress.completed,
      failed: progress.failed,
      skipped: progress.skipped,
      errors: errors.map((e) => ({ postId: e.postId, error: e.error })),
      processingTime: Date.now() - startTime,
      dryRun: false,
    };

    this.logger.info("Bulk content analysis completed", {
      ...summary,
      resultsCount: results.length,
      avgScore: results.length > 0 ? (results.reduce((sum, r) => sum + r.score, 0) / results.length).toFixed(1) : 0,
    });

    return { results, summary };
  }

  /**
   * Process metadata update for a single post
   */
  private async processMetadataUpdate(postId: number, params: SEOToolParams): Promise<void> {
    // Check cache first
    const cacheKey = `bulk-meta-${postId}`;
    if (this.cacheManager && !params.force) {
      const cached = this.cacheManager.get(cacheKey);
      if (cached) {
        this.logger.debug("Using cached metadata", { postId });
        return;
      }
    }

    // Fetch post data
    const post = await this.client.getPost(postId);
    if (!post) {
      throw new Error(`Post ${postId} not found`);
    }

    // Generate metadata
    const metadataParams = {
      postId,
      ...(params.focusKeywords && { focusKeywords: params.focusKeywords }),
      ...(params.site && { site: params.site }),
    };
    const metadata = await this.metaGenerator.generateMetadata(post, metadataParams);

    if (!params.dryRun) {
      // Apply updates to WordPress (this would need actual WordPress API calls)
      // For now, we just simulate the update
      this.logger.debug("Applying metadata updates", {
        postId,
        titleLength: metadata.title.length,
        descriptionLength: metadata.description.length,
      });

      // In a real implementation, you would call:
      // await this.client.updatePost(postId, { meta: metadata });
    }

    // Cache the result
    if (this.cacheManager) {
      this.cacheManager.set(cacheKey, metadata, 3600); // 1 hour
    }
  }

  /**
   * Process content analysis for a single post
   */
  private async processContentAnalysis(postId: number, params: SEOToolParams): Promise<SEOAnalysisResult> {
    // Check cache first
    const cacheKey = `bulk-analysis-${postId}-${params.analysisType || "full"}`;
    if (this.cacheManager && !params.force) {
      const cached = this.cacheManager.get(cacheKey);
      if (cached) {
        this.logger.debug("Using cached analysis", { postId });
        return cached as SEOAnalysisResult;
      }
    }

    // Fetch post data
    const post = await this.client.getPost(postId);
    if (!post) {
      throw new Error(`Post ${postId} not found`);
    }

    // Analyze content
    const analysisParams = {
      postId,
      analysisType: params.analysisType || ("full" as const),
      ...(params.site && { site: params.site }),
      ...(params.focusKeywords && { focusKeywords: params.focusKeywords }),
    };
    const analysisResult = await this.contentAnalyzer.analyzePost(post as WordPressPost, analysisParams);

    // Cache the result
    if (this.cacheManager) {
      this.cacheManager.set(cacheKey, analysisResult, 21600); // 6 hours
    }

    return analysisResult;
  }

  /**
   * Retry operation with exponential backoff
   */
  private async retryOperation<T>(
    operation: () => Promise<T>,
    error: BulkOperationError,
  ): Promise<{ success: boolean; result?: T; error: BulkOperationError }> {
    let delay = this.config.retryDelayMs;

    for (let attempt = 2; attempt <= this.config.maxRetries + 1; attempt++) {
      await this.delay(delay);

      try {
        const result = await operation();
        this.logger.debug("Retry successful", {
          postId: error.postId,
          attempt,
          delay,
        });

        return { success: true, result, error };
      } catch (retryError) {
        error.attempts = attempt;
        error.error = retryError instanceof Error ? retryError.message : String(retryError);

        // Exponential backoff
        delay = Math.min(delay * 2, this.config.maxRetryDelayMs);

        this.logger.debug("Retry failed", {
          postId: error.postId,
          attempt,
          error: error.error,
          nextDelay: delay,
        });
      }
    }

    return { success: false, error };
  }

  /**
   * Check if an error is retryable
   */
  private isRetryableError(error: unknown): boolean {
    if (!error) return false;

    const errorMessage = error instanceof Error ? error.message : String(error);

    // Network errors are retryable
    if (
      errorMessage.includes("ECONNREFUSED") ||
      errorMessage.includes("timeout") ||
      errorMessage.includes("503") ||
      errorMessage.includes("502") ||
      errorMessage.includes("504")
    ) {
      return true;
    }

    // Authentication errors are not retryable
    if (errorMessage.includes("401") || errorMessage.includes("403")) {
      return false;
    }

    // Not found errors are not retryable
    if (errorMessage.includes("404")) {
      return false;
    }

    // Rate limiting is retryable
    if (errorMessage.includes("429")) {
      return true;
    }

    // Default to non-retryable for safety
    return false;
  }

  /**
   * Create batches from an array of items
   */
  private createBatches<T>(items: T[], batchSize: number): T[][] {
    const batches: T[][] = [];

    for (let i = 0; i < items.length; i += batchSize) {
      batches.push(items.slice(i, i + batchSize));
    }

    return batches;
  }

  /**
   * Promise-based delay utility
   */
  private delay(ms: number): Promise<void> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  /**
   * Get current configuration
   */
  getConfig(): BulkOperationConfig {
    return { ...this.config };
  }

  /**
   * Update configuration
   */
  updateConfig(config: Partial<BulkOperationConfig>): void {
    this.config = { ...this.config, ...config };
    this.logger.debug("Configuration updated", { config: this.config });
  }
}
