import { Injectable } from "@angular/core";
import { TwitterApiProvider } from "../twitter-api/twitter-api";
import { P2pDatabaseGunProvider } from "../p2p-database-gun/p2p-database-gun";
import { P2pStorageIpfsProvider } from "../p2p-storage-ipfs/p2p-storage-ipfs";
import { CryptoProvider } from "../crypto/crypto";
import { Storage } from "@ionic/storage";
@Injectable()
export class FeedProvider {
  friends;
  userId: string;

  constructor(
    private twitter: TwitterApiProvider,
    private gun: P2pDatabaseGunProvider,
    private ipfs: P2pStorageIpfsProvider,
    private cryptoUtils: CryptoProvider,
    private storage: Storage
  ) {
    this.storage.get("userId").then(userId => (this.userId = userId));
  }

  public async loadUserTimeline(
    userId,
    oldestPublicTweet?,
    oldestPrivateTweet?
  ) {
    const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
    // Fetch tweets from Twitter
    let tweets = await this.twitter.fetchUserTimeline(userId, maxId);
    if (tweets.length === 0) return tweets;
    tweets = tweets.filter(tweet => tweet.id_str != maxId);

    // Determine start and end of time interval to look for private tweets
    const intervalStart: Date = oldestPrivateTweet
      ? new Date(oldestPrivateTweet["created_at"])
      : new Date();
    const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);

    // Fetch private tweet hashs from P2P DB for corresponding interval
    const privateTweetHashs: object[] = await this.gun.fetchPrivateTweetHashsForUserInInterval(
      userId,
      intervalStart,
      intervalEnd
    );

    if (privateTweetHashs.length) {
      const privateTweets = await this.fetchPrivateTweets(privateTweetHashs);

      // Combine and sort tweets
      return tweets
        .concat(privateTweets)
        .sort((a, b) => this.sortByDateAsc(a, b));
    } else {
      return tweets;
    }
  }

  public async loadHomeTimeline(oldestPublicTweet?, oldestPrivateTweet?) {
    // Fetch tweets from Twitter
    const maxId = oldestPublicTweet ? oldestPublicTweet["id_str"] : undefined;
    let tweets = await this.twitter.fetchHomeFeed(maxId);
    tweets = tweets.filter(tweet => tweet.id_str != maxId);

    // Determine start and end of time interval to look for private tweets
    const intervalStart: Date = oldestPrivateTweet
      ? new Date(oldestPrivateTweet["created_at"])
      : new Date();
    const intervalEnd: Date = this.getOldestTweetTimestamp(tweets);

    // Fetch user's friends
    const friends = await this.getCachedFriends(this.userId);

    // Extract friends user ids and add own user id
    const friendsAndUserIds = friends
      .map(friend => friend.id_str)
      .concat([this.userId]);

    // Fetch ipfs hashs for period
    const promises = [];
    friendsAndUserIds.forEach(async accountId => {
      promises.push(
        this.gun.fetchPrivateTweetHashsForUserInInterval(
          accountId,
          intervalStart,
          intervalEnd
        )
      );
    });

    const resolvedPromises = await Promise.all(promises);
    const privateTweetHashs = resolvedPromises.reduce(
      (el, privateTweets) => privateTweets.concat(el),
      []
    );

    if (privateTweetHashs.length) {
      const privateTweets = await this.fetchPrivateTweets(privateTweetHashs);

      // Combine and sort tweets
      return tweets
        .concat(privateTweets)
        .sort((a, b) => this.sortByDateAsc(a, b));
    } else {
      return tweets;
    }
  }

  private async fetchPrivateTweets(privateTweetsData: object[]) {
    const privateTweets = [];

    // Load private tweets from P2P storage
    for (let i = 0; i < privateTweetsData.length; i++) {
      const hash = privateTweetsData[i]["hash"];
      const userId = privateTweetsData[i]["userId"];
      const timestamp = privateTweetsData[i]["created_at"];

      // fetch from IPFS
      const encryptedTweet = await this.ipfs.fetchTweet(hash);

      // Fetch public key history for user
      const publicKeyHistory: object[] = await this.cryptoUtils.fetchPublicKeyHistoryForUser(
        userId
      );

      // Decrypt tweets
      const decryptedTweet = this.cryptoUtils.decrypt(
        encryptedTweet,
        this.getPublicKeyAt(timestamp, publicKeyHistory)
      );
      privateTweets.push(JSON.parse(decryptedTweet));
    }

    // Add retweeted/quoted status
    privateTweets.map(async tweet => await this.addQuotedStatusToTweet(tweet));

    // Add original status (reply to)
    privateTweets.map(
      async tweet => await this.addOriginalStatusToTweet(tweet)
    );

    // Add user object to private tweets
    return await Promise.all(
      privateTweets.map(async tweet => await this.addUserToTweet(tweet))
    );
  }

  private getPublicKeyAt(
    timestamp: string,
    publicKeyHistory: object[]
  ): string {
    const timestampTweet = new Date(timestamp).getTime();
    for (let key of publicKeyHistory) {
      const timestampKey = new Date(key["validFrom"]).getTime();
      if (timestampTweet > timestampKey) {
        return key["key"];
      }
    }
    // todo: throw error
    return "";
  }

  private async addUserToTweet(tweet: object): Promise<object> {
    tweet["user"] = await this.twitter.fetchUser(tweet["user_id"]);
    return tweet;
  }

  private async addQuotedStatusToTweet(tweet: object): Promise<object> {
    if (!tweet["quoted_status_id"]) return tweet;
    const quoted_status = await this.twitter.fetchTweet(
      tweet["quoted_status_id"]
    );
    tweet["quoted_status"] = quoted_status["data"];
    return tweet;
  }

  private async addOriginalStatusToTweet(tweet: object): Promise<object> {
    if (!tweet["in_reply_to_status_id"]) return tweet;
    const originalTweet = await this.twitter.fetchTweet(
      tweet["in_reply_to_status_id"]
    );
    tweet["in_reply_to_screen_name"] =
      originalTweet["data"]["user"]["screen_name"];
    return tweet;
  }

  private getOldestTweetTimestamp(tweets): Date {
    if (tweets.length < 15) {
      // End of timeline is reached - load all private tweets
      return new Date("2018-04-01T00:00:00");
    } else {
      const lastTweetTimestamp = tweets[tweets.length - 1].created_at;
      return new Date(lastTweetTimestamp);
    }
  }

  private sortByDateAsc(a, b) {
    const dateA = new Date(a.created_at);
    const dateB = new Date(b.created_at);

    if (dateA > dateB) {
      return -1;
    } else if (dateA < dateB) {
      return 1;
    } else {
      return 0;
    }
  }

  private async getCachedFriends(userId) {
    // Cache friends for 15 minutes to avoid unnecessary  API calls
    if (!this.friends || (Date.now() - this.friends.lastUpdate) / 900000 > 15) {
      this.friends = {
        friendList: await this.twitter.fetchFriends(userId),
        lastUpdate: Date.now()
      };
    }
    return this.friends.friendList;
  }
}
