/**
 * General operation tracker that works with any provider
 */

import type { Logger, MongoDBClient } from '@stock-bot/types';
import type { BaseOperationProvider } from './BaseOperationProvider';
import type {
  BulkOperationUpdate,
  OperationComponentOptions,
  OperationConfig,
  OperationStats,
  OperationUpdate,
  StaleSymbolOptions,
  SymbolWithOperations
} from './types';

/**
 * Tracks operation execution times and states for symbols
 */
export class OperationTracker {
  private mongodb: MongoDBClient;
  private logger: Logger;
  private provider: BaseOperationProvider;
  private indexesCreated: Set<string> = new Set();

  constructor(
    { mongodb, logger }: OperationComponentOptions,
    provider: BaseOperationProvider
  ) {
    this.mongodb = mongodb;
    this.logger = logger;
    this.provider = provider;
  }

  /**
   * Initialize the tracker and create indexes
   */
  async initialize(): Promise<void> {
    const { name } = this.provider.getProviderConfig();
    this.logger.info(`Initializing operation tracker for provider: ${name}`);

    // Initialize provider first
    await this.provider.initialize();

    // Create indexes for all operations
    const operations = this.provider.getOperations();
    for (const operation of operations) {
      await this.createOperationIndexes(operation);
    }

    this.logger.info(`Operation tracker initialized for ${name}`, {
      operationCount: operations.length
    });
  }

  /**
   * Create indexes for efficient operation queries
   */
  private async createOperationIndexes(operation: OperationConfig): Promise<void> {
    const { collectionName, symbolField } = this.provider.getProviderConfig();
    
    if (this.indexesCreated.has(operation.name)) {
      return;
    }

    try {
      const indexes = [
        // Index for finding stale symbols
        { 
          [`operations.${operation.name}.lastSuccessAt`]: 1, 
          [symbolField]: 1 
        },
        // Index for finding by last record date
        { 
          [`operations.${operation.name}.lastRecordDate`]: 1, 
          [symbolField]: 1 
        },
        // Index for operation status
        { 
          [`operations.${operation.name}.status`]: 1, 
          [symbolField]: 1 
        }
      ];

      // Add crawl-specific indexes for crawl operations
      if (operation.type === 'crawl' || operation.type === 'intraday_crawl') {
        indexes.push(
          { 
            [`operations.${operation.name}.finished`]: 1, 
            [symbolField]: 1 
          },
          {
            [`operations.${operation.name}.newestDateReached`]: 1,
            [symbolField]: 1
          },
          {
            [`operations.${operation.name}.oldestDateReached`]: 1,
            [symbolField]: 1
          }
        );
      }

      const collection = this.mongodb.collection(collectionName);
      
      for (const indexSpec of indexes) {
        try {
          await collection.createIndex(indexSpec, {
            background: true,
            name: `op_${operation.name}_${Object.keys(indexSpec).join('_')}`
          });
        } catch (indexError) {
          // Log but don't fail if index already exists or we hit the limit
          if (indexError.message?.includes('too many indexes') || indexError.code === 86) {
            this.logger.warn(`Skipping index creation due to limit`, {
              operation: operation.name,
              index: Object.keys(indexSpec).join('_'),
              error: indexError.message
            });
          } else if (indexError.code === 11000 || indexError.code === 85) {
            // Index already exists
            this.logger.debug(`Index already exists`, {
              operation: operation.name,
              index: Object.keys(indexSpec).join('_')
            });
          } else {
            throw indexError;
          }
        }
      }

      this.indexesCreated.add(operation.name);
      
      this.logger.debug(`Created indexes for operation: ${operation.name}`, {
        provider: this.provider.getProviderConfig().name,
        operation: operation.name,
        indexCount: indexes.length
      });
    } catch (error) {
      this.logger.error(`Failed to create indexes for operation: ${operation.name}`, {
        error,
        provider: this.provider.getProviderConfig().name
      });
      throw error;
    }
  }

  /**
   * Update symbol operation status
   */
  async updateSymbolOperation(
    symbol: string,
    operationName: string,
    data: OperationUpdate
  ): Promise<void> {
    const { collectionName, symbolField, name: providerName } = this.provider.getProviderConfig();
    
    // Validate operation exists
    this.provider.validateOperation(operationName);

    // Call before hook
    await this.provider.beforeOperationUpdate(symbol, operationName, data);

    const update: any = {
      $set: {
        [`operations.${operationName}.lastRunAt`]: new Date(),
        [`operations.${operationName}.status`]: data.status,
        updated_at: new Date()
      }
    };

    // Only update lastSuccessAt on successful operations
    if (data.status === 'success') {
      update.$set[`operations.${operationName}.lastSuccessAt`] = new Date();
    }

    if (data.lastRecordDate) {
      update.$set[`operations.${operationName}.lastRecordDate`] = data.lastRecordDate;
    }

    if (data.recordCount !== undefined) {
      update.$set[`operations.${operationName}.recordCount`] = data.recordCount;
    }

    if (data.error) {
      update.$set[`operations.${operationName}.error`] = data.error;
    }

    if (data.metadata) {
      update.$set[`operations.${operationName}.metadata`] = data.metadata;
    }

    // Handle crawl-specific fields
    if (data.finished !== undefined) {
      update.$set[`operations.${operationName}.finished`] = data.finished;
    }
    if (data.oldestDateReached) {
      update.$set[`operations.${operationName}.oldestDateReached`] = data.oldestDateReached;
    }
    if (data.newestDateReached) {
      update.$set[`operations.${operationName}.newestDateReached`] = data.newestDateReached;
    }
    if (data.lastProcessedDate) {
      update.$set[`operations.${operationName}.lastProcessedDate`] = data.lastProcessedDate;
    }
    if (data.totalDaysProcessed !== undefined) {
      update.$set[`operations.${operationName}.totalDaysProcessed`] = data.totalDaysProcessed;
    }
    if (data.lastCrawlDirection) {
      update.$set[`operations.${operationName}.lastCrawlDirection`] = data.lastCrawlDirection;
    }
    if (data.targetOldestDate) {
      update.$set[`operations.${operationName}.targetOldestDate`] = data.targetOldestDate;
    }

    this.logger.debug('Updating symbol operation with filter', {
      collectionName,
      filter: { [symbolField]: symbol },
      symbolField,
      symbol,
      operation: operationName
    });

    const updateResult = await this.mongodb.updateOne(
      collectionName,
      { [symbolField]: symbol },
      update
    );

    this.logger.debug('Update result', {
      matched: updateResult.matchedCount,
      modified: updateResult.modifiedCount,
      symbol,
      operation: operationName
    });

    // Call after hook
    await this.provider.afterOperationUpdate(symbol, operationName, data);

    this.logger.debug('Updated symbol operation', {
      provider: providerName,
      symbol,
      operation: operationName,
      status: data.status
    });
  }

  /**
   * Bulk update symbol operations for performance
   */
  async bulkUpdateSymbolOperations(updates: BulkOperationUpdate[]): Promise<void> {
    if (updates.length === 0) {return;}

    const { collectionName, symbolField, name: providerName } = this.provider.getProviderConfig();

    // Group by operation for validation
    const operationGroups = new Map<string, BulkOperationUpdate[]>();
    for (const update of updates) {
      this.provider.validateOperation(update.operation);
      
      const group = operationGroups.get(update.operation) || [];
      group.push(update);
      operationGroups.set(update.operation, group);
    }

    // Call before hooks
    for (const update of updates) {
      await this.provider.beforeOperationUpdate(update.symbol, update.operation, update.data);
    }

    // Build bulk operations
    const bulkOps = updates.map(({ symbol, operation, data }) => {
      const update: any = {
        $set: {
          [`operations.${operation}.lastRunAt`]: new Date(),
          [`operations.${operation}.status`]: data.status,
          updated_at: new Date()
        }
      };

      if (data.status === 'success') {
        update.$set[`operations.${operation}.lastSuccessAt`] = new Date();
      }

      if (data.lastRecordDate) {
        update.$set[`operations.${operation}.lastRecordDate`] = data.lastRecordDate;
      }

      if (data.recordCount !== undefined) {
        update.$set[`operations.${operation}.recordCount`] = data.recordCount;
      }

      if (data.error) {
        update.$set[`operations.${operation}.error`] = data.error;
      }

      if (data.metadata) {
        update.$set[`operations.${operation}.metadata`] = data.metadata;
      }

      // Handle crawl-specific fields
      if (data.finished !== undefined) {
        update.$set[`operations.${operation}.finished`] = data.finished;
      }
      if (data.oldestDateReached) {
        update.$set[`operations.${operation}.oldestDateReached`] = data.oldestDateReached;
      }
      if (data.newestDateReached) {
        update.$set[`operations.${operation}.newestDateReached`] = data.newestDateReached;
      }
      if (data.lastProcessedDate) {
        update.$set[`operations.${operation}.lastProcessedDate`] = data.lastProcessedDate;
      }
      if (data.totalDaysProcessed !== undefined) {
        update.$set[`operations.${operation}.totalDaysProcessed`] = data.totalDaysProcessed;
      }
      if (data.lastCrawlDirection) {
        update.$set[`operations.${operation}.lastCrawlDirection`] = data.lastCrawlDirection;
      }
      if (data.targetOldestDate) {
        update.$set[`operations.${operation}.targetOldestDate`] = data.targetOldestDate;
      }

      return {
        updateOne: {
          filter: { [symbolField]: symbol },
          update
        }
      };
    });

    const collection = this.mongodb.collection(collectionName);
    const result = await collection.bulkWrite(bulkOps, { ordered: false });

    // Call after hooks
    for (const update of updates) {
      await this.provider.afterOperationUpdate(update.symbol, update.operation, update.data);
    }

    this.logger.debug('Bulk updated symbol operations', {
      provider: providerName,
      totalUpdates: updates.length,
      modified: result.modifiedCount,
      operations: Array.from(operationGroups.keys())
    });
  }

  /**
   * Get symbols that need processing for an operation
   */
  async getStaleSymbols(
    operationName: string,
    options: StaleSymbolOptions = {}
  ): Promise<Array<{ symbol: any; lastRun?: Date; lastSuccess?: Date; operations?: any }>> {
    const { collectionName, symbolField } = this.provider.getProviderConfig();
    const { 
      limit = 1000, 
      excludeSymbols = [], 
      activeOnly = true,
      symbolFilter
    } = options;

    this.provider.validateOperation(operationName);

    const cutoffDate = options.notRunSince || (() => {
      const date = new Date();
      const hours = options.minHoursSinceRun || this.provider.getDefaultStaleHours(operationName);
      date.setHours(date.getHours() - hours);
      return date;
    })();

    const filter: any = {
      $or: [
        { [`operations.${operationName}.lastSuccessAt`]: { $lt: cutoffDate } },
        { [`operations.${operationName}.lastSuccessAt`]: { $exists: false } },
        { [`operations.${operationName}`]: { $exists: false } }
      ]
    };

    if (activeOnly) {
      filter.active = { $ne: false };
    }

    if (excludeSymbols.length > 0) {
      filter[symbolField] = { $nin: excludeSymbols };
    }

    // Add symbol filter if provided
    if (symbolFilter?.symbol) {
      filter[symbolField] = symbolFilter.symbol;
    }

    const symbols = await this.mongodb.find(collectionName, filter, {
      limit,
      projection: { }, // Return all fields
      sort: { [`operations.${operationName}.lastSuccessAt`]: 1 }
    });

    return symbols.map(doc => ({
      symbol: doc,
      lastRun: doc.operations?.[operationName]?.lastRunAt,
      lastSuccess: doc.operations?.[operationName]?.lastSuccessAt,
      operations: doc.operations
    }));
  }

  /**
   * Get symbols for crawl operations
   */
  async getSymbolsForCrawl(
    operationName: string,
    options: {
      limit?: number;
      includeFinished?: boolean;
    } = {}
  ): Promise<SymbolWithOperations[]> {
    const { collectionName, symbolField } = this.provider.getProviderConfig();
    const { limit = 100, includeFinished = false } = options;

    this.provider.validateOperation(operationName);

    const filter: any = {
      active: { $ne: false }
    };

    if (!includeFinished) {
      filter[`operations.${operationName}.finished`] = { $ne: true };
    }

    const symbols = await this.mongodb.find(collectionName, filter, {
      limit,
      projection: {
        [symbolField]: 1,
        [`operations.${operationName}`]: 1
      },
      sort: {
        [`operations.${operationName}.lastRunAt`]: 1
      }
    });

    return symbols.map(doc => ({
      symbol: doc[symbolField],
      lastRecordDate: doc.operations?.[operationName]?.lastRecordDate,
      operationStatus: doc.operations?.[operationName]
    }));
  }

  /**
   * Mark crawl as finished
   */
  async markCrawlFinished(
    symbol: string,
    operationName: string,
    oldestDateReached: Date,
    newestDateReached?: Date
  ): Promise<void> {
    await this.updateSymbolOperation(symbol, operationName, {
      status: 'success',
      finished: true,
      oldestDateReached,
      newestDateReached: newestDateReached || new Date()
    });

    this.logger.info('Marked crawl as finished', {
      provider: this.provider.getProviderConfig().name,
      symbol,
      operation: operationName,
      oldestDateReached,
      newestDateReached
    });
  }

  /**
   * Get symbols for intraday crawl with gap detection
   */
  async getSymbolsForIntradayCrawl(
    operationName: string,
    options: {
      limit?: number;
      targetOldestDate?: Date;
      includeNewDataGaps?: boolean;
      symbolFilter?: { symbol?: string };
    } = {}
  ): Promise<Array<SymbolWithOperations & { gaps?: { forward?: boolean; backward?: boolean } }>> {
    const { collectionName, symbolField } = this.provider.getProviderConfig();
    const { limit = 100, targetOldestDate, includeNewDataGaps = true, symbolFilter } = options;

    this.provider.validateOperation(operationName);

    // Build filter
    const filter: any = {
      active: { $ne: false }
    };

    // Add symbol filter if provided
    if (symbolFilter?.symbol) {
      filter[symbolField] = symbolFilter.symbol;
    }

    // Get all symbols that either:
    // 1. Have never been crawled
    // 2. Are not finished
    // 3. Have gaps (new data since last crawl)
    const orConditions = [
      { [`operations.${operationName}`]: { $exists: false } },
      { [`operations.${operationName}.finished`]: { $ne: true } }
    ];

    if (includeNewDataGaps) {
      const yesterday = new Date();
      yesterday.setDate(yesterday.getDate() - 1);
      yesterday.setHours(0, 0, 0, 0);
      
      orConditions.push({
        [`operations.${operationName}.newestDateReached`]: { $lt: yesterday }
      });
    }

    filter.$or = orConditions;

    const symbols = await this.mongodb.find(collectionName, filter, {
      limit,
      projection: {
        [symbolField]: 1,
        [`operations.${operationName}`]: 1,
        // Include common fields that might be needed
        symbol: 1,
        exchange: 1,
        qmSearchCode: 1
      },
      sort: {
        [`operations.${operationName}.lastRunAt`]: 1
      }
    });

    return symbols.map(doc => {
      const opStatus = doc.operations?.[operationName];
      
      // Determine gaps (only backward since we removed forward crawling)
      const gaps: { forward?: boolean; backward?: boolean } = {};
      
      if (opStatus) {
        // Only check for backward gap (historical data)
        if (!opStatus.finished) {
          gaps.backward = true;
          if (targetOldestDate && opStatus.oldestDateReached) {
            gaps.backward = new Date(opStatus.oldestDateReached) > targetOldestDate;
          }
        }
      } else {
        // Never crawled, needs backward crawl
        gaps.backward = true;
      }

      return {
        symbol: doc[symbolField],
        lastRecordDate: opStatus?.lastRecordDate,
        operationStatus: opStatus,
        gaps,
        // Include other potentially useful fields
        exchange: doc.exchange,
        qmSearchCode: doc.qmSearchCode || doc[symbolField]
      } as SymbolWithOperations & { gaps?: { forward?: boolean; backward?: boolean } };
    });
  }

  /**
   * Check if intraday crawl is complete
   */
  async isIntradayCrawlComplete(
    symbol: string,
    operationName: string,
    targetOldestDate: Date
  ): Promise<boolean> {
    const { collectionName, symbolField } = this.provider.getProviderConfig();
    
    const doc = await this.mongodb.findOne(collectionName, {
      [symbolField]: symbol
    }, {
      projection: { [`operations.${operationName}`]: 1 }
    });

    if (!doc?.operations?.[operationName]) {
      return false;
    }

    const opStatus = doc.operations[operationName];
    
    // Check if explicitly marked as finished
    if (opStatus.finished) {
      return true;
    }

    // Check if we've reached the target oldest date
    if (opStatus.oldestDateReached && targetOldestDate) {
      return new Date(opStatus.oldestDateReached) <= targetOldestDate;
    }

    return false;
  }

  /**
   * Get symbols that need data updates based on last record date
   */
  async getSymbolsNeedingUpdate(
    operationName: string,
    options: {
      lastRecordBefore?: Date;
      neverRun?: boolean;
      limit?: number;
    } = {}
  ): Promise<SymbolWithOperations[]> {
    const { collectionName, symbolField } = this.provider.getProviderConfig();
    const { limit = 500 } = options;

    this.provider.validateOperation(operationName);

    const filter: any = {
      active: { $ne: false }
    };

    if (options.neverRun) {
      filter[`operations.${operationName}`] = { $exists: false };
    } else if (options.lastRecordBefore) {
      filter.$or = [
        { [`operations.${operationName}.lastRecordDate`]: { $lt: options.lastRecordBefore } },
        { [`operations.${operationName}`]: { $exists: false } }
      ];
    }

    const symbols = await this.mongodb.find(collectionName, filter, {
      limit,
      projection: {
        [symbolField]: 1,
        [`operations.${operationName}.lastRecordDate`]: 1,
        [`operations.${operationName}`]: 1
      },
      sort: { [`operations.${operationName}.lastRecordDate`]: 1 }
    });

    return symbols.map(doc => ({
      symbol: doc[symbolField],
      lastRecordDate: doc.operations?.[operationName]?.lastRecordDate,
      operationStatus: doc.operations?.[operationName]
    }));
  }

  /**
   * Get operation statistics
   */
  async getOperationStats(operationName: string): Promise<OperationStats> {
    const { collectionName } = this.provider.getProviderConfig();
    const operation = this.provider.getOperation(operationName);
    
    if (!operation) {
      throw new Error(`Unknown operation: ${operationName}`);
    }

    const collection = this.mongodb.collection(collectionName);
    
    const [
      total,
      processed,
      successful,
      failed,
      staleStats,
      crawlStats,
      recordStats
    ] = await Promise.all([
      // Total symbols
      collection.countDocuments({}),
      
      // Processed symbols
      collection.countDocuments({
        [`operations.${operationName}`]: { $exists: true }
      }),
      
      // Successful symbols
      collection.countDocuments({
        [`operations.${operationName}.status`]: 'success'
      }),
      
      // Failed symbols
      collection.countDocuments({
        [`operations.${operationName}.status`]: 'failure'
      }),
      
      // Stale symbols
      this.countStaleSymbols(operationName, operation.defaultStaleHours || 24),
      
      // Crawl stats (if applicable)
      (operation.type === 'crawl' || operation.type === 'intraday_crawl')
        ? collection.countDocuments({
            [`operations.${operationName}.finished`]: true
          })
        : Promise.resolve(undefined),
      
      // Average records
      this.calculateAverageRecords(operationName)
    ]);

    const result: OperationStats = {
      totalSymbols: total,
      processedSymbols: processed,
      staleSymbols: staleStats,
      successfulSymbols: successful,
      failedSymbols: failed
    };

    if (crawlStats !== undefined) {
      result.finishedCrawls = crawlStats;
    }

    if (recordStats !== undefined) {
      result.avgRecordsPerSymbol = recordStats;
    }

    return result;
  }

  /**
   * Count stale symbols
   */
  private async countStaleSymbols(operationName: string, staleHours: number): Promise<number> {
    const { collectionName } = this.provider.getProviderConfig();
    const collection = this.mongodb.collection(collectionName);
    
    const staleDate = new Date();
    staleDate.setHours(staleDate.getHours() - staleHours);

    return collection.countDocuments({
      $or: [
        { [`operations.${operationName}.lastSuccessAt`]: { $lt: staleDate } },
        { [`operations.${operationName}`]: { $exists: false } }
      ]
    });
  }

  /**
   * Calculate average records per symbol
   */
  private async calculateAverageRecords(operationName: string): Promise<number | undefined> {
    const { collectionName } = this.provider.getProviderConfig();
    const collection = this.mongodb.collection(collectionName);
    
    const aggregation = await collection.aggregate([
      {
        $match: {
          [`operations.${operationName}.recordCount`]: { $exists: true }
        }
      },
      {
        $group: {
          _id: null,
          avgRecords: { $avg: `$operations.${operationName}.recordCount` }
        }
      }
    ]).toArray();

    if (aggregation.length > 0 && aggregation[0].avgRecords) {
      return Math.round(aggregation[0].avgRecords);
    }

    return undefined;
  }

  /**
   * Get provider info
   */
  getProviderInfo() {
    return this.provider.getProviderConfig();
  }
}