import {
  QueueConfiguration,
  QueueState,
  QueueUsage,
  QueueItem,
  QueueStatus,
} from './types';
import { StoreScope } from '@activepieces/pieces-framework';

/**
 * Central utility handling all queue storage and scheduling logic.
 * All data is persisted via the piece `context.store` (PROJECT scope).
 */
export class QueueManager {
  /* ---------------------------------------------------------------------- */
  /* Helpers – configuration, state & usage                                 */
  /* ---------------------------------------------------------------------- */

  static async getQueueConfiguration(
    context: any,
    queueId: string,
  ): Promise<QueueConfiguration | null> {
    return await context.store.get(`queue_config_${queueId}`, StoreScope.PROJECT);
  }

  /**
   * NOTE: Activepieces key-value store cannot list keys, so a full “list queues”
   *       is not possible without maintaining an external index. Stub returns [].
   */
  static async listAllQueues(/* context: any */): Promise<QueueConfiguration[]> {
    return [];
  }

  static async getQueueState(
    context: any,
    queueId: string,
  ): Promise<QueueState> {
    const key = `queue_state_${queueId}`;
    return (
      (await context.store.get(key, StoreScope.PROJECT)) || {
        queueId,
        lastReleaseTime: 0,
        itemCount: 0,
        currentExecutingItem: null,
        lastExecutedTime: 0,
        version: 0,
      }
    );
  }

  static async getQueueUsage(
    context: any,
    queueId: string,
  ): Promise<QueueUsage> {
    const now = new Date();
    const dateKey = now.toISOString().split('T')[0];
    const hourKey = now.toISOString().slice(0, 13);

    const key = `queue_usage_${queueId}`;
    const usage: QueueUsage =
      (await context.store.get(key, StoreScope.PROJECT)) || {
        date: dateKey,
        hour: hourKey,
        dailyCount: 0,
        hourlyCount: 0,
      };

    // daily rollover
    if (usage.date !== dateKey) {
      usage.date = dateKey;
      usage.dailyCount = 0;
    }
    // hourly rollover
    if (usage.hour !== hourKey) {
      usage.hour = hourKey;
      usage.hourlyCount = 0;
    }
    return usage;
  }

  /* ---------------------------------------------------------------------- */
  /* Status & rate-limit helpers                                            */
  /* ---------------------------------------------------------------------- */

  static async getQueueStatus(
    context: any,
    queueId: string,
  ): Promise<QueueStatus | null> {
    const queue = await this.getQueueConfiguration(context, queueId);
    if (!queue) return null;

    const state = await this.getQueueState(context, queueId);
    const usage = await this.getQueueUsage(context, queueId);

    const isWithinActiveHours = queue.activeHours
      ? this.isWithinActiveHours(queue.activeHours)
      : true;

    const nextActiveWindow =
      queue.activeHours && !isWithinActiveHours
        ? this.getNextActiveWindow(queue.activeHours)
        : undefined;

    return {
      pendingItems: state.itemCount,
      currentlyExecuting: state.currentExecutingItem,
      lastExecutedTime: state.lastExecutedTime
        ? new Date(state.lastExecutedTime).toISOString()
        : null,
      dailyUsed: usage.dailyCount,
      dailyLimit: queue.dailyLimit,
      dailyRemaining:
        queue.dailyLimit > 0 ? queue.dailyLimit - usage.dailyCount : null,
      hourlyUsed: usage.hourlyCount,
      hourlyLimit: queue.hourlyLimit,
      hourlyRemaining:
        queue.hourlyLimit > 0 ? queue.hourlyLimit - usage.hourlyCount : null,
      isWithinActiveHours,
      nextActiveWindow,
    };
  }

  /** Check daily/hourly rate limits and return availability. */
  static async checkRateLimits(
    context: any,
    queueId: string,
  ): Promise<{
    canSend: boolean;
    nextAvailableTime?: number;
    dailyRemaining?: number;
    hourlyRemaining?: number;
  }> {
    const queue = await this.getQueueConfiguration(context, queueId);
    if (!queue) return { canSend: true };

    if (queue.dailyLimit === 0 && queue.hourlyLimit === 0)
      return { canSend: true };

    const usage = await this.getQueueUsage(context, queueId);
    const now = new Date();

    // daily
    if (queue.dailyLimit > 0 && usage.dailyCount >= queue.dailyLimit) {
      const tomorrow = new Date(now);
      tomorrow.setDate(tomorrow.getDate() + 1);
      tomorrow.setHours(0, 0, 0, 0);
      return {
        canSend: false,
        nextAvailableTime: tomorrow.getTime(),
        dailyRemaining: 0,
        hourlyRemaining:
          queue.hourlyLimit > 0 ? queue.hourlyLimit - usage.hourlyCount : 0,
      };
    }

    // hourly
    if (queue.hourlyLimit > 0 && usage.hourlyCount >= queue.hourlyLimit) {
      const nextHour = new Date(now);
      nextHour.setHours(nextHour.getHours() + 1, 0, 0, 0);
      return {
        canSend: false,
        nextAvailableTime: nextHour.getTime(),
        dailyRemaining:
          queue.dailyLimit > 0 ? queue.dailyLimit - usage.dailyCount : undefined,
        hourlyRemaining: 0,
      };
    }

    return {
      canSend: true,
      dailyRemaining:
        queue.dailyLimit > 0 ? queue.dailyLimit - usage.dailyCount : undefined,
      hourlyRemaining:
        queue.hourlyLimit > 0 ? queue.hourlyLimit - usage.hourlyCount : undefined,
    };
  }

  /* ---------------------------------------------------------------------- */
  /* Delay calculations                                                     */
  /* ---------------------------------------------------------------------- */

  static calculateDelayMs(
    delayType: 'fixed' | 'random',
    delayUnit: 'seconds' | 'minutes' | 'hours' | 'days',
    delayValue?: number,
    delayMin?: number,
    delayMax?: number,
  ): number {
    const mult: Record<typeof delayUnit, number> = {
      seconds: 1000,
      minutes: 60 * 1000,
      hours: 60 * 60 * 1000,
      days: 24 * 60 * 60 * 1000,
    };

    if (delayType === 'fixed') {
      return Math.floor((delayValue || 0) * mult[delayUnit]);
    }
    const min = delayMin || 0;
    const max = delayMax || 0;
    const rnd = Math.random() * (max - min) + min;
    return Math.floor(rnd * mult[delayUnit]);
  }

  static async calculateItemDelay(
    context: any,
    queue: QueueConfiguration,
    limits: any,
  ): Promise<{
    delayMs: number;
    limitDelay: number;
    activeHoursDelay: number;
  }> {
    let baseDelay = this.calculateDelayMs(
      queue.delayType,
      queue.delayUnit,
      queue.delayValue,
      queue.delayMin,
      queue.delayMax,
    );

    let limitDelay = 0;
    let activeHoursDelay = 0;

    if (!limits.canSend && limits.nextAvailableTime) {
      const now = Date.now();
      limitDelay = Math.max(0, limits.nextAvailableTime - now);
      baseDelay = Math.max(baseDelay, limitDelay);
    }

    if (queue.activeHours) {
      // First check if we're currently within active hours
      const isCurrentlyActive = this.isWithinActiveHours(queue.activeHours);
      
      if (!isCurrentlyActive) {
        // Only apply active hours delay if we're outside active hours
        const nextActive = this.getNextActiveTime(queue.activeHours);
        const now = Date.now();
        if (nextActive > now) {
          activeHoursDelay = nextActive - now;
          baseDelay = Math.max(baseDelay, activeHoursDelay);
        }
      }
      // If we're within active hours, no additional delay needed
    }

    return { delayMs: baseDelay, limitDelay, activeHoursDelay };
  }

  /* ---------------------------------------------------------------------- */
  /* Active-hours helpers                                                   */
  /* ---------------------------------------------------------------------- */

  static isWithinActiveHours(activeHours: any): boolean {
    if (!activeHours?.schedule) return true;

    const now = new Date();
    const tz = activeHours.timezone || 'UTC';
    const local = new Date(now.toLocaleString('en-US', { timeZone: tz }));

    const day = local
      .toLocaleDateString('en-US', { weekday: 'long' })
      .toLowerCase();
    const time = local.toTimeString().slice(0, 5); // HH:MM

    const sched = activeHours.schedule[day];
    if (!sched || !sched.enabled) return false;
    if (sched.start && sched.end) {
      return time >= sched.start && time <= sched.end;
    }
    return true;
  }

  static getNextActiveTime(activeHours: any): number {
    if (!activeHours?.schedule) return Date.now();

    const now = new Date();
    const tz = activeHours.timezone || 'UTC';

    for (let i = 0; i < 7; i++) {
      const cand = new Date(now);
      cand.setDate(cand.getDate() + i);

      const local = new Date(cand.toLocaleString('en-US', { timeZone: tz }));
      const day = local
        .toLocaleDateString('en-US', { weekday: 'long' })
        .toLowerCase();
      const sched = activeHours.schedule[day];

      if (sched && sched.enabled && sched.start) {
        const [h, m] = sched.start.split(':').map(Number);
        cand.setHours(h, m, 0, 0);
        if (cand.getTime() > now.getTime()) return cand.getTime();
      }
    }
    return now.getTime() + 24 * 60 * 60 * 1000;
  }

  static getNextActiveWindow(activeHours: any): string {
    return new Date(this.getNextActiveTime(activeHours)).toISOString();
  }

  /* ---------------------------------------------------------------------- */
  /* Core scheduling – add, validate, update, cleanup                       */
  /* ---------------------------------------------------------------------- */

  static async addItemToQueue(
    context: any,
    queueId: string,
    targetAction: string,
    actionConfig: any,
    delayMs: number,
  ): Promise<{ queueItemId: string; releaseTime: number }> {
    const MAX_RETRIES = 5;
    let attempt = 0;
    const queue = await this.getQueueConfiguration(context, queueId);
    if (!queue) throw new Error(`Queue not found: ${queueId}`);

    while (attempt < MAX_RETRIES) {
      try {
        const ts = Date.now();
        const rand = Math.random().toString(36).substr(2, 9);
        const queueItemId = `${queueId}_${ts}_${rand}`;

        const stateKey = `queue_state_${queueId}`;
        const currentState: QueueState =
          (await context.store.get(stateKey, StoreScope.PROJECT)) || {
            queueId,
            lastReleaseTime: 0,
            itemCount: 0,
            currentExecutingItem: null,
            lastExecutedTime: 0,
            version: 0,
          };

        // Get actual latest release time from queued items (FIFO)
        const itemsListKey = `queue_${queueId}_items`;
        const itemIds: string[] = (await context.store.get(itemsListKey, StoreScope.PROJECT)) || [];
        
        let latestReleaseTime = 0;
        for (const itemId of itemIds) {
          const item = await this.getQueueItem(context, itemId);
          if (item && item.status === 'scheduled' && item.releaseTime > latestReleaseTime) {
            latestReleaseTime = item.releaseTime;
          }
        }
        
        const earliest = ts + delayMs;
        const nextRelease = Math.max(
          earliest,
          latestReleaseTime + delayMs,
        );
        const uniqueOffset = Math.floor(Math.random() * 1000);
        let finalReleaseTime = nextRelease + uniqueOffset;
        
        // Apply active hours constraints AFTER calculating queue position
        if (queue.activeHours) {
          const isCurrentlyActive = this.isWithinActiveHours(queue.activeHours);
          
          if (!isCurrentlyActive) {
            // Only push to next active window if we're outside active hours
            const nextActive = this.getNextActiveTime(queue.activeHours);
            if (nextActive > finalReleaseTime) {
              finalReleaseTime = nextActive;
            }
          }
          // If we're within active hours, keep the calculated time
        }

        // slot reservation
        const slotKey = `queue_time_${queueId}_${finalReleaseTime}`;
        if (await context.store.get(slotKey, StoreScope.PROJECT)) {
          attempt++;
          await new Promise((r) => setTimeout(r, 50 * attempt));
          continue;
        }
        await context.store.put(slotKey, queueItemId, StoreScope.PROJECT);

        const queueItem: QueueItem = {
          id: queueItemId,
          queueId,
          releaseTime: finalReleaseTime,
          targetAction,
          actionConfig,
          status: 'scheduled',
          createdAt: ts,
        };
        await context.store.put(
          `queue_item_${queueItemId}`,
          queueItem,
          StoreScope.PROJECT,
        );

        const newState: QueueState = {
          ...currentState,
          lastReleaseTime: finalReleaseTime,
          itemCount: currentState.itemCount + 1,
          version: currentState.version + 1,
        };

        const verify = await context.store.get(stateKey, StoreScope.PROJECT);
        if (verify && verify.version !== currentState.version) {
          // optimistic lock failed – cleanup
          await context.store.delete(`queue_item_${queueItemId}`, StoreScope.PROJECT);
          await context.store.delete(slotKey, StoreScope.PROJECT);
          attempt++;
          await new Promise((r) => setTimeout(r, 100 * attempt));
          continue;
        }

        await context.store.put(stateKey, newState, StoreScope.PROJECT);

        // track item list
        const list: string[] =
          (await context.store.get(itemsListKey, StoreScope.PROJECT)) || [];
        list.push(queueItemId);
        await context.store.put(itemsListKey, list, StoreScope.PROJECT);

        return { queueItemId, releaseTime: finalReleaseTime };
      } catch (err: any) {
        attempt++;
        if (attempt >= MAX_RETRIES)
          throw new Error(
            `Failed to add item after ${MAX_RETRIES} attempts: ${err.message}`,
          );
        await new Promise((r) => setTimeout(r, 100 * attempt));
      }
    }
    throw new Error('Failed to add item to queue');
  }

  static async getQueueItem(
    context: any,
    queueItemId: string,
  ): Promise<QueueItem | null> {
    return await context.store.get(`queue_item_${queueItemId}`, StoreScope.PROJECT);
  }

  static async updateItemStatus(
    context: any,
    queueItemId: string,
    status: QueueItem['status'],
    error?: string,
  ) {
    const item = await this.getQueueItem(context, queueItemId);
    if (item) {
      item.status = status;
      if (error) item.lastError = error;
      await context.store.put(`queue_item_${queueItemId}`, item, StoreScope.PROJECT);
    }
  }

  static async validateQueuePosition(
    context: any,
    queueItemId: string,
  ): Promise<{
    isValid: boolean;
    shouldRepause: boolean;
    newReleaseTime?: number;
    reason?: string;
  }> {
    const item = await this.getQueueItem(context, queueItemId);
    if (!item)
      return { isValid: false, shouldRepause: false, reason: 'Item not found' };

    const now = Date.now();
    const state = await this.getQueueState(context, item.queueId);

    if (
      state.currentExecutingItem &&
      state.currentExecutingItem !== queueItemId
    ) {
      return {
        isValid: false,
        shouldRepause: true,
        newReleaseTime: now + 5000,
        reason: 'Another item executing',
      };
    }

    const nextOk = await this.checkIfNextInQueue(context, item);
    if (!nextOk) {
      const queue = await this.getQueueConfiguration(context, item.queueId);
      if (!queue)
        return {
          isValid: false,
          shouldRepause: false,
          reason: 'Queue not found',
        };

      const delayMs = this.calculateDelayMs(
        queue.delayType,
        queue.delayUnit,
        queue.delayValue,
        queue.delayMin,
        queue.delayMax,
      );
      const newRelease = state.lastReleaseTime + delayMs;

      item.releaseTime = newRelease;
      await context.store.put(`queue_item_${item.id}`, item, StoreScope.PROJECT);
      return {
        isValid: false,
        shouldRepause: true,
        newReleaseTime: newRelease,
        reason: 'Not next in queue',
      };
    }

    if (now >= item.releaseTime) {
      state.currentExecutingItem = queueItemId;
      await context.store.put(`queue_state_${item.queueId}`, state, StoreScope.PROJECT);
      return { isValid: true, shouldRepause: false };
    }

    return {
      isValid: false,
      shouldRepause: true,
      newReleaseTime: item.releaseTime,
      reason: 'Not time yet',
    };
  }

  static async checkIfNextInQueue(
    context: any,
    currentItem: QueueItem,
  ): Promise<boolean> {
    const list: string[] =
      (await context.store.get(
        `queue_${currentItem.queueId}_items`,
        StoreScope.PROJECT,
      )) || [];

    for (const id of list) {
      if (id === currentItem.id) continue;
      const it = await this.getQueueItem(context, id);
      if (it && it.status === 'scheduled' && it.releaseTime < currentItem.releaseTime)
        return false;
    }
    return true;
  }

  static async markExecutionComplete(
    context: any,
    queueId: string,
    queueItemId: string,
  ) {
    const state = await this.getQueueState(context, queueId);
    state.currentExecutingItem = null;
    state.lastExecutedTime = Date.now();
    state.itemCount = Math.max(0, state.itemCount - 1);
    await context.store.put(`queue_state_${queueId}`, state, StoreScope.PROJECT);

    const listKey = `queue_${queueId}_items`;
    const list: string[] =
      (await context.store.get(listKey, StoreScope.PROJECT)) || [];
    await context.store.put(
      listKey,
      list.filter((id) => id !== queueItemId),
      StoreScope.PROJECT,
    );
  }

  static async cleanupQueueItem(
    context: any,
    queueId: string,
    queueItemId: string,
    releaseTime: number,
  ) {
    await context.store.delete(
      `queue_time_${queueId}_${releaseTime}`,
      StoreScope.PROJECT,
    );
    await context.store.delete(`queue_item_${queueItemId}`, StoreScope.PROJECT);
  }

  static async incrementUsageCounters(context: any, queueId: string) {
    const usage = await this.getQueueUsage(context, queueId);
    usage.dailyCount++;
    usage.hourlyCount++;
    await context.store.put(`queue_usage_${queueId}`, usage, StoreScope.PROJECT);

    const queue = await this.getQueueConfiguration(context, queueId);
    if (queue) {
      queue.totalProcessed++;
      queue.lastUsed = Date.now();
      await context.store.put(`queue_config_${queueId}`, queue, StoreScope.PROJECT);
    }
  }

  /* ---------------------------------------------------------------------- */
  /* Queue Management Operations                                            */
  /* ---------------------------------------------------------------------- */

  /**
   * Get all items in a queue with their details
   */
  static async listQueueItems(context: any, queueId: string): Promise<QueueItem[]> {
    const listKey = `queue_${queueId}_items`;
    const itemIds: string[] = (await context.store.get(listKey, StoreScope.PROJECT)) || [];
    
    const items: QueueItem[] = [];
    for (const itemId of itemIds) {
      const item = await this.getQueueItem(context, itemId);
      if (item) {
        items.push(item);
      }
    }
    
    // Sort by release time
    return items.sort((a, b) => a.releaseTime - b.releaseTime);
  }

  /**
   * Clear all items from a queue
   */
  static async clearQueue(context: any, queueId: string): Promise<{
    clearedCount: number;
    queueId: string;
  }> {
    if (!queueId) {
      throw new Error('Queue ID is required for clearing queue');
    }

    if (!context) {
      throw new Error('Context is required for clearing queue');
    }

    try {
      const listKey = `queue_${queueId}_items`;
      let itemIds: string[];
      
      try {
        itemIds = (await context.store.get(listKey, StoreScope.PROJECT)) || [];
      } catch (error: any) {
        throw new Error(`Failed to get queue items list: ${error.message}`);
      }
      
      let clearedCount = 0;
      const errors: string[] = [];
      
      // Delete all queue items and their time slots
      for (const itemId of itemIds) {
        try {
          const item = await this.getQueueItem(context, itemId);
          if (item) {
            // Delete the item
            try {
              await context.store.delete(`queue_item_${itemId}`, StoreScope.PROJECT);
            } catch (error: any) {
              errors.push(`Failed to delete item ${itemId}: ${error.message}`);
              continue;
            }
            
            // Delete the time slot reservation
            try {
              const slotKey = `queue_time_${queueId}_${item.releaseTime}`;
              await context.store.delete(slotKey, StoreScope.PROJECT);
            } catch (error: any) {
              errors.push(`Failed to delete time slot for item ${itemId}: ${error.message}`);
            }
            
            clearedCount++;
          }
        } catch (error: any) {
          errors.push(`Failed to process item ${itemId}: ${error.message}`);
        }
      }
      
      // Clear the items list
      try {
        await context.store.delete(listKey, StoreScope.PROJECT);
      } catch (error: any) {
        throw new Error(`Failed to clear items list: ${error.message}`);
      }
      
      // Reset queue state
      const stateKey = `queue_state_${queueId}`;
      try {
        const currentState: QueueState = (await context.store.get(stateKey, StoreScope.PROJECT)) || {
          queueId,
          lastReleaseTime: 0,
          itemCount: 0,
          currentExecutingItem: null,
          lastExecutedTime: 0,
          version: 0,
        };
        
        const resetState: QueueState = {
          ...currentState,
          itemCount: 0,
          currentExecutingItem: null,
          lastReleaseTime: 0,  // Reset this so new items start fresh
          version: currentState.version + 1,
        };
        
        await context.store.put(stateKey, resetState, StoreScope.PROJECT);
      } catch (error: any) {
        throw new Error(`Failed to reset queue state: ${error.message}`);
      }
      
      // If there were partial errors, include them in the response
      if (errors.length > 0) {
        throw new Error(`Queue cleared with ${errors.length} errors: ${errors.join('; ')}`);
      }
      
      return {
        clearedCount,
        queueId,
      };
    } catch (error: any) {
      throw new Error(`Clear queue operation failed: ${error.message}`);
    }
  }

  /**
   * Get complete queue information including configuration and status
   */
  static async getQueueDetails(context: any, queueId: string): Promise<{
    configuration: QueueConfiguration | null;
    status: QueueStatus | null;
    state: QueueState;
    usage: QueueUsage;
  }> {
    const configuration = await this.getQueueConfiguration(context, queueId);
    const status = await this.getQueueStatus(context, queueId);
    const state = await this.getQueueState(context, queueId);
    const usage = await this.getQueueUsage(context, queueId);
    
    return {
      configuration,
      status,
      state,
      usage,
    };
  }
}
