import { ClassError, ObjectBase } from '@tomei/general';
import { FeedCutOffHistory } from '../feed-cut-off-history/feed-cut-off-history';
import { ISourceFeedAttr } from '../../interfaces/price-source-feed-attr.interface';
import { SourceFeedRepository } from './source-feed.repository';
import {
  ActivationTrigger,
  NonSourceFeedName,
  SourceFeedName,
} from '../../enum/feed.enum';
import { IPriceData } from '../../interfaces/price-data.interface';
import { FeedHistory } from '../feed-history/feed-history';
import { Feed } from '../feed/feed';
import { IFeedAttr } from '../../interfaces/price-feed-attr.interface';
import { FeedConfig } from '../feed-config/feed-config';
import { FeedHistoryRepository } from '../feed-history/feed-history.repository';
import { Op, Transaction } from 'sequelize';
import { LoginUser } from '@tomei/sso';
import { PriceSourceBase } from '../../base/PriceSourceBase';
import { TomeiPriceHistory } from '../tomei-price/tomei-price';
import {
  ActionEnum,
  Activity,
  ActivityHistoryRepository,
} from '@tomei/activity-history';

export class SourceFeed extends Feed implements ISourceFeedAttr {
  ObjectId: string;
  ObjectName: string;
  ObjectType: string;
  TableName: string;
  FeedName: SourceFeedName;
  Status: string;
  IsSourceFeedAvailableYN: string;
  IsFeedCutOffYN: string;
  CurrentCutOffHistoryId: string;
  AutomaticCutOffVariance: number;
  AutomaticCutOffMinutes: number;

  protected static _SourceRepo = new SourceFeedRepository();
  protected static _FeedHistoryRepo = new FeedHistoryRepository();
  protected static _ActivityRepo = new ActivityHistoryRepository();

  protected constructor(
    sourcefeedAttr?: ISourceFeedAttr,
    feedAttr?: IFeedAttr,
  ) {
    super(feedAttr);
    if (sourcefeedAttr) {
      this.FeedName = sourcefeedAttr.FeedName;
      this.Status = sourcefeedAttr.Status;
      this.IsSourceFeedAvailableYN = sourcefeedAttr.IsSourceFeedAvailableYN;
      this.IsFeedCutOffYN = sourcefeedAttr.IsFeedCutOffYN;
      this.CurrentCutOffHistoryId = sourcefeedAttr.CurrentCutOffHistoryId;
      this.AutomaticCutOffVariance = sourcefeedAttr.AutomaticCutOffVariance;
      this.AutomaticCutOffMinutes = sourcefeedAttr.AutomaticCutOffMinutes;
    }
  }

  public static async init(dbTransaction: any, FeedName?: string) {
    try {
      if (FeedName) {
        const sourcefeed = await SourceFeed._SourceRepo.findByPk(
          FeedName,
          dbTransaction,
        );
        const feed = await Feed._Repo.findByPk(FeedName, dbTransaction);
        if (sourcefeed) {
          return new SourceFeed(sourcefeed, feed);
        } else {
          throw new ClassError('SourceFeed', 'FeedErrMsg00', 'Feed Not Found');
        }
      }
      return new SourceFeed();
    } catch (error) {
      throw new ClassError('SourceFeed', 'FeedErrMsg01', error.message);
    }
  }

  public async save(dbTransaction: any) {
    try {
      await super.save(dbTransaction);
      const sourcefeed = await SourceFeed._SourceRepo.findOne({
        where: {
          FeedName: this.FeedName,
        },
        transaction: dbTransaction,
      });
      const payload: ISourceFeedAttr = {
        FeedName: this.FeedName,
        Status: this.Status,
        IsSourceFeedAvailableYN: this.IsSourceFeedAvailableYN,
        IsFeedCutOffYN: this.IsFeedCutOffYN,
        CurrentCutOffHistoryId: this.CurrentCutOffHistoryId,
        AutomaticCutOffVariance: this.AutomaticCutOffVariance,
        AutomaticCutOffMinutes: this.AutomaticCutOffMinutes,
      };
      if (sourcefeed) {
        await SourceFeed._SourceRepo.update(payload, {
          where: {
            FeedName: this.FeedName,
          },
          transaction: dbTransaction,
        });
      } else {
        await SourceFeed._SourceRepo.create(payload, {
          transaction: dbTransaction,
        });
      }
    } catch (error) {
      throw new ClassError('Feed', 'FeedErrMsg02', error.message);
    }
  }

  public async getCutOffDetails(dbTransaction: any) {
    try {
      if (this.CurrentCutOffHistoryId) {
        return await FeedCutOffHistory.init(
          this.CurrentCutOffHistoryId,
          dbTransaction,
        );
      }
      return null;
    } catch (error) {
      throw new ClassError('Feed', 'FeedErrMsg03', error.message);
    }
  }

  public async recordFeedPrice(
    priceData: IPriceData,
    dbTransaction: any,
  ): Promise<FeedHistory> {
    try {
      // Based on the current price feed from MKS, set the following priceFeedData:
      // Date & Time
      // Product - Set it to the product name as per No. 1 above.
      const feedName = this.FeedName;
      // Source Feed Buy - The original buy price from MKS. NA is the MKS feed is unavailable.
      const sourceFeedBuyPrice = priceData.BuyPrice;
      // Source Feed Sell - The original sell price from MKS. NA is the MKS feed is unavailable.
      const sourceFeedSellPrice = priceData.SellPrice;
      // Feed Buy - Set the Feed Buy Price based on the following logic:
      // If Manual Price is Activated, set the Feed Buy price to Manual Buy Price.
      // Else If Cut-Off is Activated be it by the automatic circuit breaker or manually, set the Feed Buy price to NA.
      // Else
      // If MKS feed is available, set the Feed Buy price to MKS Buy price.
      // Else If MKS Feed is unavailable, set the Feed Buy Price to NA.
      // Feed Sell - Set the Feed Sell Price based on the following logic:
      // If Manual Price is Activated, set the Feed Sell price to Manual Sell Price.
      // Else If Cut-Off is Activated be it by the automatic circuit breaker or manually, set the Feed Sell price to NA.
      // Else
      // If MKS feed is available, set the Feed Sell price to MKS Sell price.
      // Else If MKS Feed is unavailable, set the Feed Buy Price to NA.
      let FeedBuyPrice: number;
      let FeedSellPrice: number;
      // MCActivatedYN (Manual Cut-Off) - Yes or No, depending if Manual Cut Off has been activated.
      let IsManualCutOffYN = 'N';
      // CBActivatedYN (Circuit Breaker) - Yes or No, depending if Circuit Breaker has been activated.
      let IsCircuitBreakerActivatedYN = 'N';
      if (this.IsManualPriceActivatedYN === 'Y') {
        //Get latest price history
        const manualPriceHistory = await FeedHistory.getLatestPrice(
          this.FeedName,
          this.FeedName.split('/')[1],
          null,
          dbTransaction,
        );
        FeedBuyPrice = manualPriceHistory.FeedBuyPrice;
        FeedSellPrice = manualPriceHistory.FeedSellPrice;
      } else if (this.IsFeedCutOffYN === 'Y') {
        FeedBuyPrice = 0;
        FeedSellPrice = 0;

        //Retrive the cut off details
        const feedCutOffHistory = await FeedCutOffHistory.init(
          this.CurrentCutOffHistoryId,
          dbTransaction,
        );
        if (feedCutOffHistory.ActivationTrigger === ActivationTrigger.MANUAL) {
          IsManualCutOffYN = 'Y';
        } else {
          IsCircuitBreakerActivatedYN = 'Y';
        }
      } else {
        FeedBuyPrice = priceData.SellPrice;
        FeedSellPrice = priceData.SellPrice;
      }
      // SFAvailableYN (Source Feed) - Yes or No, depending if MKS feed is available.
      const isSourceFeedAvailableYN = priceData.IsSourceFeedAvailableYN;
      // MCActivatedYN (Manual Cut-Off) - Yes or No, depending if Manual Cut Off has been activated.
      const isManualPriceActivatedYN = this.IsManualPriceActivatedYN;
      // Store the price feed information in price_PriceFeedHistory table using the saveFeedPrice method.
      let feedHistory = await FeedHistory.init(dbTransaction);
      feedHistory = await feedHistory.saveFeedPrice(
        {
          FeedName: feedName,
          DateTime: new Date(),
          Currency: priceData.Currency,
          SourceFeedBuyPrice: sourceFeedBuyPrice,
          SourceFeedSellPrice: sourceFeedSellPrice,
          FeedBuyPrice: FeedBuyPrice,
          FeedSellPrice: FeedSellPrice,
          IsSourceFeedAvailableYN: isSourceFeedAvailableYN,
          IsCircuitBreakerActivatedYN: IsCircuitBreakerActivatedYN,
          IsManualPriceActivatedYN: isManualPriceActivatedYN,
          IsManualCutOffYN: IsManualCutOffYN,
        },
        dbTransaction,
      );
      // Check Feed Buy and Feed Sell:
      // If Feed Buy and Feed Sell is not NA, null or 0, then set Feed.Status  in the price_Feed table to "Available".
      // If Feed Buy and Feed Sell is NA, null or 0, then set Feed.Status  in the price_Feed table to "Unavailable".
      if (FeedBuyPrice !== 0 && FeedSellPrice !== 0) {
        this.Status = 'Available';
      } else {
        this.Status = 'Unavailable';
      }

      // Save the Feed Status to the price_Feed table using the save method.
      await this.save(dbTransaction);
      return feedHistory;
    } catch (error) {
      throw error;
    }
  }

  private async getPreviousSourceFeedConfig(dbTransaction: any): Promise<
    | {
        AutomaticCutOffVariance: number;
        AutomaticCutOffMinutes: number;
        UpdatedAt: Date;
      }
    | false
  > {
    try {
      const previousActivity = await SourceFeed._ActivityRepo.findOne({
        where: {
          EntityType: 'SourceFeed',
          EntityId: this.FeedName,
          Action: ActionEnum.UPDATE,
          Description: 'Update Circuit Breaker Config for :' + this.FeedName,
        },
        transaction: dbTransaction,
        order: [['PerformedAt', 'DESC']],
      });
      if (previousActivity) {
        const previousConfig: {
          AutomaticCutOffVariance: number;
          AutomaticCutOffMinutes: number;
          FeedName: string;
          Status: string;
          IsSourceFeedAvailableYN: string;
          IsFeedCutOffYN: string;
          CurrentCutOffHistoryId: string;
        } = JSON.parse(previousActivity.EntityValueBefore);
        return {
          AutomaticCutOffVariance: previousConfig.AutomaticCutOffVariance,
          AutomaticCutOffMinutes: previousConfig.AutomaticCutOffMinutes,
          UpdatedAt: previousActivity.PerformedAt,
        };
      }
      return false; // No previous configuration found
    } catch (error) {
      throw error;
    }
  }

  public async circuitBreakerCheck(
    priceData: IPriceData,
    priceSource: PriceSourceBase<SourceFeed>,
    dbTransaction?: any,
  ): Promise<boolean> {
    // Circuit breaker or automatic price cut-off will be activated based on the settings in feed config.
    // The variance check will be applied to the Source Prices from price_PriceFeedHistory table.
    // For example, if the user sets up 5% within 5 minutes, the system will automatically cut off the feed
    // when there is a 5% variance in price compared to the price 5 minutes back. This applies to both Sell and Buy prices.

    try {
      let automaticCutOffVariance = this.AutomaticCutOffVariance; // Default variance from the current configuration
      let automaticCutOffMinutes = this.AutomaticCutOffMinutes; // Default time window from the current configuration

      // Step 1: Retrieve the previous configuration from the database
      const previousConfig =
        await this.getPreviousSourceFeedConfig(dbTransaction);

      // Step 2: Check if a previous configuration exists
      if (previousConfig) {
        const currentTime = new Date().getTime(); // Get the current timestamp
        const previousConfigUpdatedTime = previousConfig.UpdatedAt.getTime(); // Timestamp when the previous config was updated
        const currentConfigTimeWindow = automaticCutOffMinutes * 60000; // Convert current time window to milliseconds

        // Step 3: Determine the effective start time for observing the new configuration
        // This ensures the new configuration only takes effect after the previous time window has elapsed
        const effectiveStartTime = Math.max(
          previousConfigUpdatedTime + currentConfigTimeWindow,
          currentTime,
        );

        // Step 4: Decide whether to use the previous or current configuration
        if (currentTime < effectiveStartTime) {
          // If the current time is before the effective start time, continue using the previous configuration
          automaticCutOffVariance = previousConfig.AutomaticCutOffVariance;
          automaticCutOffMinutes = previousConfig.AutomaticCutOffMinutes;
        } else {
          // If the current time is after the effective start time, switch to the current configuration
          automaticCutOffVariance = this.AutomaticCutOffVariance;
          automaticCutOffMinutes = this.AutomaticCutOffMinutes;
        }
      } else {
        // If no previous configuration exists, use the current configuration values
        automaticCutOffVariance = this.AutomaticCutOffVariance;
        automaticCutOffMinutes = this.AutomaticCutOffMinutes;
      }

      // Step 5: Retrieve the feed history for the last `automaticCutOffMinutes` minutes
      const feedHistories = await SourceFeed._FeedHistoryRepo.findAll({
        where: {
          FeedName: this.FeedName,
          DateTime: {
            [Op.gte]: new Date(
              new Date().getTime() - this.AutomaticCutOffMinutes * 60000,
            ),
          },
        },
        order: [['DateTime', 'DESC']],
        transaction: dbTransaction,
      });

      // Step 6: Skip circuit breaker check if any feed history is manually activated
      if (
        feedHistories.some(
          ({ IsManualPriceActivatedYN }) => IsManualPriceActivatedYN === 'Y',
        )
      ) {
        return true; // Skip the circuit breaker check
      }

      // Step 7: Filter out feed histories with non-zero prices
      const validFeedHistories = feedHistories.filter(
        ({ SourceFeedBuyPrice, SourceFeedSellPrice }) =>
          SourceFeedBuyPrice !== 0 && SourceFeedSellPrice !== 0,
      );

      // Step 8: Skip circuit breaker check if no valid feed histories exist
      if (validFeedHistories.length === 0) return true;

      // Step 9: Extract buy and sell prices from feed histories
      const buyPrices = validFeedHistories.map(
        ({ SourceFeedBuyPrice }) => SourceFeedBuyPrice,
      );
      const sellPrices = validFeedHistories.map(
        ({ SourceFeedSellPrice }) => SourceFeedSellPrice,
      );

      // Step 10: Include current price data in the range calculations
      buyPrices.push(priceData.BuyPrice);
      sellPrices.push(priceData.SellPrice);

      // Step 11: Calculate min and max for buy and sell prices
      const [minBuyPrice, maxBuyPrice] = [
        Math.min(...buyPrices),
        Math.max(...buyPrices),
      ];
      const [minSellPrice, maxSellPrice] = [
        Math.min(...sellPrices),
        Math.max(...sellPrices),
      ];

      // Step 12: Calculate variances for buy and sell prices
      const buyVariance = ((maxBuyPrice - minBuyPrice) / minBuyPrice) * 100;
      const sellVariance = ((maxSellPrice - minSellPrice) / minSellPrice) * 100;

      // Step 13: Trigger circuit breaker if variance exceeds threshold and feed is not already cut off
      if (
        (buyVariance > this.AutomaticCutOffVariance ||
          sellVariance > this.AutomaticCutOffVariance) &&
        this.IsFeedCutOffYN === 'N'
      ) {
        await this.cutOff(priceSource, dbTransaction); // Trigger the circuit breaker
        return false; // Circuit breaker triggered
      }

      return true; // Circuit breaker not triggered
    } catch (error) {
      throw error; // Handle any errors that occur during the process
    }
  }

  public async cutOff(
    priceSource: PriceSourceBase<SourceFeed>,
    dbTransaction: any,
    activatedById?: string,
  ) {
    try {
      //create a new feed cut off history
      let feedCutOffHistory = await FeedCutOffHistory.init();

      feedCutOffHistory.FeedName = this.FeedName;
      let activatedBy = null;
      let isManualCutOff = 'N';
      let isCircuitBreakerActivated = 'N';
      if (activatedById) {
        //If the feed is cut off manually, set the activation trigger to manual
        const user = await LoginUser.init(null, parseInt(activatedById));
        activatedBy = user.FullName;
        feedCutOffHistory.ActivationTrigger = ActivationTrigger.MANUAL;
        isManualCutOff = 'Y';
      } else {
        //If the feed is cut off automatically, set the activation trigger to limit breach
        feedCutOffHistory.ActivationTrigger = ActivationTrigger.LIMIT_BREACH;
        isCircuitBreakerActivated = 'Y';
      }
      //create a new feed cut off history record
      feedCutOffHistory = await feedCutOffHistory.create(
        dbTransaction,
        activatedById,
      );

      //create a new feed history record
      let feedHistory = await FeedHistory.init(dbTransaction);
      const fetchResult = await priceSource.fetchPrice({
        [this.FeedName]: this,
      });
      const priceData = fetchResult[this.FeedName];
      feedHistory = await feedHistory.saveFeedPrice(
        {
          FeedName: this.FeedName,
          DateTime: new Date(),
          Currency: priceData.Currency,
          SourceFeedBuyPrice: priceData.BuyPrice,
          SourceFeedSellPrice: priceData.SellPrice,
          FeedBuyPrice: 0,
          FeedSellPrice: 0,
          IsSourceFeedAvailableYN: priceData.IsSourceFeedAvailableYN,
          IsManualCutOffYN: isManualCutOff,
          IsCircuitBreakerActivatedYN: isCircuitBreakerActivated,
          IsManualPriceActivatedYN: 'N',
        },
        dbTransaction,
      );
      //create a new tomei price history
      await this.createTomeiPriceHistory(feedHistory, dbTransaction);

      //update the feed status to cut off
      this.IsFeedCutOffYN = 'Y';
      this.CurrentCutOffHistoryId = feedCutOffHistory.FeedCutOffHistoryId;
      //save the feed status
      await this.save(dbTransaction);
    } catch (error) {
      throw error;
    }
  }

  public async deactivateCutOff(
    priceSource: PriceSourceBase<SourceFeed>,
    dbTransaction: any,
    deactivatedById: string,
  ) {
    try {
      //deactivate the feed cut off
      await FeedCutOffHistory.deactivate(
        this.FeedName,
        deactivatedById,
        dbTransaction,
      );
      //update the feed status
      this.IsFeedCutOffYN = 'N';
      //update the feed record in the database
      await this.save(dbTransaction);

      //Retrieve the price from Source Feed
      const fetchResult = await priceSource.fetchPrice({
        [this.FeedName]: this,
      });
      const priceData = fetchResult[this.FeedName];

      //Record the feed price
      const feedHistory = await this.recordFeedPrice(priceData, dbTransaction);

      //Create a new tomei price history
      await this.createTomeiPriceHistory(feedHistory, dbTransaction);
    } catch (error) {
      throw error;
    }
  }

  public async updateCircuitBreakerConfig(
    AutomaticCutOffVariance: number,
    AutomaticCutOffMinutes: number,
    dbTransaction: any,
    loginUser: LoginUser,
  ) {
    try {
      const EntityValueBefore = {
        AutomaticCutOffVariance: this.AutomaticCutOffVariance,
        AutomaticCutOffMinutes: this.AutomaticCutOffMinutes,
        FeedName: this.FeedName,
        Status: this.Status,
        IsSourceFeedAvailableYN: this.IsSourceFeedAvailableYN,
        IsFeedCutOffYN: this.IsFeedCutOffYN,
        CurrentCutOffHistoryId: this.CurrentCutOffHistoryId,
      };

      this.AutomaticCutOffVariance = AutomaticCutOffVariance;
      this.AutomaticCutOffMinutes = AutomaticCutOffMinutes;

      await SourceFeed._SourceRepo.update(
        {
          AutomaticCutOffVariance: this.AutomaticCutOffVariance,
          AutomaticCutOffMinutes: this.AutomaticCutOffMinutes,
        },
        {
          where: {
            FeedName: this.FeedName,
          },
          transaction: dbTransaction,
        },
      );

      // Log the activity history for updating the circuit breaker configuration
      const activity = new Activity();
      activity.ActivityId = activity.createId();
      activity.Description =
        'Update Circuit Breaker Config for :' + this.FeedName;
      activity.Action = ActionEnum.UPDATE;
      activity.EntityType = 'SourceFeed';
      activity.EntityId = this.FeedName;
      activity.EntityValueBefore = JSON.stringify(EntityValueBefore);
      activity.EntityValueAfter = JSON.stringify({
        AutomaticCutOffVariance: this.AutomaticCutOffVariance,
        AutomaticCutOffMinutes: this.AutomaticCutOffMinutes,
        FeedName: this.FeedName,
        Status: this.Status,
        IsSourceFeedAvailableYN: this.IsSourceFeedAvailableYN,
        IsFeedCutOffYN: this.IsFeedCutOffYN,
        CurrentCutOffHistoryId: this.CurrentCutOffHistoryId,
      });
      await activity.create(loginUser.ObjectId, dbTransaction);
    } catch (error) {
      throw error;
    }
  }

  public static async getAllSourceFeed(
    dbTransaction: any,
  ): Promise<SourceFeed[]> {
    try {
      const sourceFeeds = await SourceFeed._SourceRepo.findAll({
        transaction: dbTransaction,
      });
      return sourceFeeds.map((sourceFeed) => new SourceFeed(sourceFeed));
    } catch (error) {
      throw error;
    }
  }
}
