import _ from 'lodash';
import moment from 'moment';
import Fut from 'fut-promise';
import request from 'request';
import * as types from './bidTypes';
import cycle from './logic';
import { getApi } from '../utils/ApiUtil';
import { setCredits } from './account';
import { findPrice } from './player';

const filter = {
  type: 'player',
  start: 0,
  num: 16,
};

let biddingTimeout;
let cycleTimeout;
let marketRequest;

export function start() {
  return async (dispatch, getState) => {
    dispatch({ type: types.START_BIDDING });
    dispatch(setCycleCount(0));
    dispatch(clearMessages());
    const state = getState();
    if (parseFloat(state.settings.autoStop, 10)) {
      dispatch(addMessage('warn', `Bidding will automatically stop after ${state.settings.autoStop} hours.`));
      biddingTimeout = window.setTimeout(() => {
        dispatch(addMessage('warn', `Bidding automatically stopped after ${state.settings.autoStop} hours.`));
        dispatch(stop());
      }, Math.round(state.settings.autoStop * 60 * 60 * 1000)); // autoStop is in hours
    }
    await dispatch(cycle());
  };
}

export function addMessage(level = 'log', msg, error = null) {
  return { type: types.ADD_MESSAGE, log: { level, msg: `[${moment().format('HH:mm:ss')}] ${msg}`, error } };
}

export function clearMessages() {
  return { type: types.CLEAR_MESSAGES };
}

export function setCycleCount(count = 0) {
  return { type: types.SET_CYCLES, count };
}

export function setWatched(watched) {
  return { type: types.SET_WATCH, watched };
}

export function updateWatched(id, count) {
  return { type: types.UPDATE_WATCH, id, count };
}

export function setListed(listed) {
  return { type: types.SET_LISTED, listed };
}

export function updateListed(id, count) {
  return { type: types.UPDATE_LISTED, id, count };
}

export function setTrades(trades) {
  return { type: types.SET_TRADES, trades };
}

export function updateTrades(id, tradeResult) {
  return { type: types.UPDATE_TRADES, id, tradeResult };
}

export function setBINStatus(won) {
  return { type: types.SET_BIN_STATUS, won };
}

export function snipe(player, settings) {
  return async (dispatch, getState) => {
    let state = getState();
    let api = getApi(state.account.email);
    dispatch(addMessage('log', `Preparing to snipe ${player.name}...`));
    // Snipe
    const binFilter = _.merge({}, filter, {
      definitionId: player.id,
      maxb: player.price.buy,
    });
    let binResponse;
    try {
      binResponse = await api.search(binFilter);
      // auctionInfo is not always an empty array
      binResponse.auctionInfo = binResponse.auctionInfo || [];
    } catch (e) {
      dispatch(addMessage('error', `Error searching for BIN on ${player.name}`, e));
      binResponse = { auctionInfo: [] };
    }
    dispatch(addMessage('log', `${binResponse.auctionInfo.length} BIN found for ${player.name}...`));
    // drop RPM for sniping
    api = getApi(state.account.email, settings.rpm > 20 ? settings.rpm : 20);
    for (const trade of binResponse.auctionInfo) {
      // refresh state every trade
      state = getState();
      if (
        // We are still bidding
        state.bid.bidding
        // We have enough credits
        && state.account.credits > settings.minCredits
        && state.account.credits > trade.buyNowPrice
        // The buy price is the gte the BIN price
        && player.price.buy >= trade.buyNowPrice
        // We are below our cap for this player
        && _.get(state, `bid.listed.${player.id}`, 0) < settings.maxCard
        // The card has at least one contract
        && trade.itemData.contract > 0
      ) {
        // Buy it!
        let tradeResult = {};
        try {
          const snipeResponse = await api.placeBid(trade.tradeId, trade.buyNowPrice);
          dispatch(setCredits(snipeResponse.credits));
          tradeResult = _.get(snipeResponse, 'auctionInfo[0]', {});
        } catch (e) {
          dispatch(addMessage('error', `Error placing BIN bid $${trade.buyNowPrice} on ${player.name}`, e));
        }
        // tradeResult = {
        //   bidState: 'buyNow',
        //   tradeState: 'closed',
        //   tradeId: trade.tradeId,
        //   currentBid: trade.buyNowPrice
        // };
        // Was this a success?
        if (tradeResult.tradeState === 'closed' && tradeResult.bidState === 'buyNow') {
          dispatch(addMessage('success', `Bought for BIN (${trade.buyNowPrice}) on ${player.name}`));
          dispatch(setBINStatus(true));
          // Increment number of trades won
          if (state.bid.listed[player.id] === undefined) {
            dispatch(updateListed(player.id, 1));
          } else {
            dispatch(updateListed(player.id, state.bid.listed[player.id] + 1));
          }
        } else {
          // TODO: do something about this
          dispatch(addMessage('warn', `Could not snipe ${player.name}`));
        }
      }
    }
    api = getApi(state.account.email, settings.rpm);
  };
}

export function placeBid(player, settings) {
  return async (dispatch, getState) => {
    if (!settings.snipeOnly) {
      let state = getState();
      let api = getApi(state.account.email);
      dispatch(addMessage('log', `Getting ready to search auctions for ${player.name}...`));
      const bidFilter = _.merge({}, filter, {
        definitionId: player.id,
        macr: player.price.buy,
      });
      let searchResults = 0;
      let resultsWithinTimeframe = [];
      try {
        const bidResponse = await api.search(bidFilter);
        // auctionInfo is not always an empty array
        bidResponse.auctionInfo = bidResponse.auctionInfo || [];
        searchResults = bidResponse.auctionInfo.length;
        resultsWithinTimeframe = _.filter(
          bidResponse.auctionInfo,
          trade => trade.expires <= (settings.bidUntilMin * 60));
      } catch (e) {
        dispatch(addMessage('error', `Error searching auctions for ${player.name}`, e));
      }
      dispatch(addMessage('log', `${searchResults} results (${resultsWithinTimeframe.length} in last ${settings.bidUntilMin} minutes)`));
      if (searchResults > 0 && resultsWithinTimeframe.length === searchResults) {
        // TODO: Increment page number and search again
      }
      for (const trade of resultsWithinTimeframe) {
        // refresh state every trade
        state = getState();
        const listed = _.get(state.bid.listed, player.id, 0);
        if (
          // We are still bidding
          state.bid.bidding
          // We haven't placed too many bids
          // (Twice as many as max card - listed, assuming 50% win chance)
          && _.get(state.bid.watched, player.id, 0) < (settings.maxCard - listed) * 2
          // TODO: Keep track of how long we are inside this loop to set the
          //       lower limit, ensuring we do not attempt to bid on expired
          //       items.
          && trade.expires > 0
          // We have enough credits
          && state.account.credits > settings.minCredits
          // We are below our cap for this player
          && listed < settings.maxCard
          // We are not bidding on something that is already active in our watchlist
          && state.bid.trades[trade.tradeId] === undefined
          // The card has at least one contract
          && trade.itemData.contract > 0
        ) {
          // Get the latest status of this trade
          let latestTrade;
          try {
            const status = await api.getStatus([trade.tradeId]);
            dispatch(setCredits(status.credits));
            latestTrade = _.get(status, 'auctionInfo[0]', trade);
          } catch (e) {
            latestTrade = trade;
          }
          // Set our bid
          let bid;
          if (latestTrade.currentBid) {
            bid = Fut.calculateNextHigherPrice(latestTrade.currentBid);
          } else {
            bid = latestTrade.startingBid;
          }

          // If the next step up is our max, go ahead and bid max
          const nextBid = Fut.calculateNextHigherPrice(bid);
          if (nextBid === Fut.calculateValidPrice(player.price.buy)) {
            bid = nextBid;
          }

          // Make sure we aren't trying to spend more than we want to
          if (latestTrade.expires > 0 && bid <= player.price.buy && bid <= state.account.credits) {
            // Bid!
            let tradeResult = {};
            api = getApi(state.account.email, 0);
            try {
              // No delay between status and bid
              const placeBidResponse = await api.placeBid(latestTrade.tradeId, bid);
              dispatch(setCredits(placeBidResponse.credits));
              tradeResult = _.get(placeBidResponse, 'auctionInfo[0]', {});
            } catch (e) {
              dispatch(addMessage('error', `Error placing bid on ${player.name}`, e));
            }
            api = getApi(state.account.email, settings.rpm);
            // tradeResult = {
            //   bidState: 'highest',
            //   tradeId: trade.tradeId,
            //   currentBid: bid
            // };
            // Was this a success?
            if (tradeResult.bidState === 'highest') {
              dispatch(addMessage('success', `Bidding ${bid} on ${player.name}`));
              // Add it to watched trades for listing and update our local watchlist
              dispatch(updateTrades(tradeResult.tradeId, tradeResult));
              state = getState();
              dispatch(setWatchlist(Object.values(state.bid.trades)));
              // Increment number of trades watched
              if (state.bid.watched[player.id] === undefined) {
                // state.bid.watched[player.id] = 1;
                dispatch(updateWatched(player.id, 1));
              } else {
                // state.bid.watched[player.id] += 1;
                dispatch(updateWatched(player.id, state.bid.watched[player.id] + 1));
              }
            } else {
              // TODO: do something about this
              dispatch(addMessage('warn', `Something happened when trying to bid on ${player.name}`));
            }
          } else if (latestTrade.expires === -1 && latestTrade.currentBid < player.price.bid) {
            dispatch(addMessage('warn', `TOO SLOW: Trade has expired for ${player.name} (sold for ${latestTrade.currentBid})`));
          } else {
            dispatch(addMessage('log', `Required bid (${bid}) is more than we are willing to pay for ${player.name} (${player.price.buy})`));
          }
        }
      }
    } else {
      dispatch(addMessage('warn', 'Skipping bidding, adjust settings to enable...'));
    }
  };
}


export function binNowToUnassigned() {
  return async (dispatch, getState) => {
    let state = getState();
    const api = getApi(state.account.email);
    if (state.bid.binWon) {
      dispatch(addMessage('log', 'Listing won BIN items...'));
      await dispatch(getUnassigned(state.account.email));
      state = getState();
      dispatch(setBINStatus(!!state.bid.unassigned.length));
      for (const i of state.bid.unassigned) {
        const baseId = Fut.getBaseId(i.resourceId);
        const trackedPlayer = _.get(state.player, `list.${baseId}`, false);
        if (trackedPlayer) {
          // Send it to the tradepile
          let pileResponse;
          try {
            pileResponse = await api.sendToTradepile(i.id);
          } catch (e) {
            dispatch(addMessage('error', 'Error sending won BIN to tradepile', e));
            pileResponse = { itemData: [{ success: false }] };
          }
          if (pileResponse.itemData[0].success) {
            try {
              await api.listItem(
                i.id,
                trackedPlayer.price.sell,
                trackedPlayer.price.bin,
                3600);
              // Increment number of trades won
              if (state.bid.listed[baseId] === undefined) {
                dispatch(updateListed(baseId, 1));
              } else {
                dispatch(updateListed(baseId, state.bid.listed[baseId] + 1));
              }
              dispatch(updateHistory(baseId, {
                id: i.id,
                bought: i.lastSalePrice,
                boughtAt: Date.now()
              }));
              dispatch(addMessage('success', `Listed ${trackedPlayer.name} for ${trackedPlayer.price.sell}/${trackedPlayer.price.bin}`));
            } catch (e) {
              dispatch(addMessage('error', 'Error listing won BIN for sale', e));
            }
          } else {
            dispatch(addMessage('warn', `Unable to send ${trackedPlayer.name} to tradepile...`));
          }
        }
      }
    }
  };
}

export function relistItems(settings) {
  return async (dispatch, getState) => {
    const state = getState();
    const api = getApi(state.account.email);
    const expired = state.bid.tradepile.filter(i => i.tradeState === 'expired');
    if (expired.length > 0) {
      dispatch(addMessage('log', 'Re-listing expired items...'));
      let relistFailed = false;
      if (settings.relistAll) {
        try {
          const relist = await api.relist();
          if (relist.code === 500) {
            relistFailed = true;
          }
        } catch (e) {
          dispatch(addMessage('error', 'Error attempting to relist all auctions', e));
          relistFailed = true;
        }
      }
      // Relist all failed? Do it manually.
      if (!settings.relistAll || relistFailed) {
        dispatch(addMessage('log', `Manually re-listing ${expired.length} players.`));
        for (const i of expired) {
          const baseId = Fut.getBaseId(i.itemData.resourceId);
          const priceDetails = _.get(state.player, `list.${baseId}.price`, false);
          try {
            if (!settings.relistAll && priceDetails) {
              await api.listItem(i.itemData.id, priceDetails.sell, priceDetails.bin, 3600);
            } else {
              await api.listItem(i.itemData.id, i.startingBid, i.buyNowPrice, 3600);
            }
          } catch (e) {
            dispatch(addMessage('error', 'Error manually re-listing player', e));
          }
        }
      }
    }
  };
}

export function logSold() {
  return async (dispatch, getState) => {
    const state = getState();
    const api = getApi(state.account.email);
    const sold = state.bid.tradepile.filter(i => i.tradeState === 'closed');

    if (sold.length > 0) {
      dispatch(addMessage('log', 'Removing sold items from tradepile...'));
      for (const i of sold) {
        // Is this a card we are tracking?
        const baseId = Fut.getBaseId(i.itemData.resourceId);
        const trackedPlayer = _.get(state.player, `list.${baseId}`, false);
        if (trackedPlayer) {
          dispatch(updateHistory(baseId, {
            id: i.itemData.id,
            sold: i.currentBid,
            soldAt: Date.now()
          }));
          dispatch(addMessage('success', `Sold ${trackedPlayer.name} for ${i.currentBid}!`));
        }
        try {
          await api.removeFromTradepile(i.tradeId);
        } catch (e) {
          dispatch(addMessage('error', 'Error removing sold item from tradepile', e));
        }
      }
      // Update tradepile when done
      await dispatch(getTradepile(state.account.email));
    }
  };
}

export function continueTracking(settings) {
  return async (dispatch, getState) => {
    let state = getState();
    const api = getApi(state.account.email);
    const tradeIds = Object.keys(state.bid.trades).filter(id => id > 0);
    if (!settings.snipeOnly && tradeIds.length) {
      dispatch(addMessage('log', `Updating status on ${tradeIds.length} active trades...`));
      let statuses;
      try {
        statuses = await api.getStatus(tradeIds);
        // auctionInfo is not always an empty array
        statuses.auctionInfo = statuses.auctionInfo || [];
        dispatch(setCredits(statuses.credits));
      } catch (e) {
        dispatch(addMessage('error', `Error getting trade statuses: ${JSON.stringify(tradeIds)}`, e));
        statuses = { auctionInfo: [] };
      }
      let newListed = false;
      for (const item of statuses.auctionInfo) {
        const baseId = Fut.getBaseId(state.bid.trades[item.tradeId].itemData.resourceId);
        const trackedPlayer = _.get(state.player, `list.${baseId}`, false);
        // Only continue if we are tracking this player, and know about this trade
        if (trackedPlayer && state.bid.trades[item.tradeId]) {
          // Handle Expired Items
          if (item.expires === -1) {
            if (item.bidState === 'highest') {
              // We won! Send to Pile!
              let pileResponse;
              try {
                pileResponse = await api.sendToTradepile(item.itemData.id);
              } catch (e) {
                dispatch(addMessage('error', 'Error sending won auction to tradepile', e));
                pileResponse = { itemData: [{ success: false }] };
              }
              if (pileResponse.itemData[0].success) {
                // List on market
                try {
                  await api.listItem(
                    item.itemData.id,
                    trackedPlayer.price.sell,
                    trackedPlayer.price.bin,
                    3600);
                  newListed = true;
                  // Increment number of trades won
                  if (state.bid.listed[baseId] === undefined) {
                    dispatch(updateListed(baseId, 1));
                  } else {
                    dispatch(updateListed(baseId, state.bid.listed[baseId] + 1));
                  }
                  dispatch(updateHistory(baseId, {
                    id: item.itemData.id,
                    bought: item.currentBid,
                    boughtAt: Date.now()
                  }));
                  dispatch(addMessage('success', `Listed ${trackedPlayer.name} for ${trackedPlayer.price.sell}/${trackedPlayer.price.bin}`));
                } catch (e) {
                  dispatch(addMessage('error', 'Error listing won auction for sale', e));
                }
              }
            } else {
              // Delete from watchlist, we lost
              try {
                await api.removeFromWatchlist(item.tradeId);
                const trades = state.bid.trades;
                delete trades[item.tradeId];
                dispatch(setWatchlist(Object.values(trades)));
                if (item.currentBid < trackedPlayer.price.buy) {
                  dispatch(addMessage('warn', `TOO SLOW: ${trackedPlayer.name} went for ${item.currentBid}`));
                } else {
                  dispatch(addMessage('log', `${trackedPlayer.name} sold for ${item.currentBid}, which is more than we want to pay`));
                }
              } catch (e) {
                dispatch(addMessage('error', 'Error removing lost auction from watchlist', e));
              }
            }
          } else if (item.bidState !== 'highest') {
            state = getState();
            // We were outbid
            const newBid = Fut.calculateNextHigherPrice(item.currentBid);
            const listed = _.get(state.bid.listed, baseId, 0);
            if (
              newBid > trackedPlayer.price.buy
              || listed >= settings.maxPlayer
            ) {
              // Remove from list if new bid is too high, or we already have too many listed
              try {
                await api.removeFromWatchlist(item.tradeId);
                const trades = state.bid.trades;
                delete trades[item.tradeId];
                dispatch(setWatchlist(Object.values(trades)));
                dispatch(addMessage('log', `${trackedPlayer.name} (${item.currentBid}) is either too expensive, or we have enough of them`));
              } catch (e) {
                dispatch(addMessage('error', 'Error removing outbid item from watchlist', e));
              }
            } else if (state.account.credits > settings.minCredits) {
              // Only continue if we have enough credits
              let tradeResult = {};
              try {
                const placeBidResponse = await api.placeBid(item.tradeId, newBid);
                dispatch(setCredits(placeBidResponse.credits));
                tradeResult = _.get(placeBidResponse, 'auctionInfo[0]', {});
              } catch (e) {
                dispatch(addMessage('error', 'Error placing additional bid on item', e));
              }
              if (tradeResult.bidState === 'highest') {
                dispatch(addMessage('success', `BIDDING WAR: Increased bid to ${newBid} on ${trackedPlayer.name}`));
              } else {
                // TODO: do something about this
                dispatch(addMessage('warn', `Something happened when trying to increase bid on ${trackedPlayer.name}`));
              }
            } else {
              dispatch(addMessage('warn', `We do not have enough credits to continue bidding! Min Credit Limit: ${settings.minCredits}`));
            }
          }
        }
      }
      if (newListed) {
        // Update tradepile if we listed something here
        await dispatch(getTradepile(state.account.email));
      }
    }
  };
}

export function keepBidding() {
  return async (dispatch, getState) => {
    const state = getState();
    if (state.bid.bidding) {
      if (
        // We don't have enough credits
        state.account.credits < state.settings.minCredits
        // We aren't watching any auctions
        && !state.bid.watchlist.length
        // We have some players listed
        && state.bid.tradepile.length
      ) {
        const timeout = state.bid.tradepile.reduce(
          (a, b) => (a < b.expires ? a : b.expires),
          60 // Wait a maximum of 60 seconds
        ) * 1000;
        dispatch(addMessage('log', `Waiting ${timeout / 1000} seconds before continuing...`));
        cycleTimeout = window.setTimeout(() => {
          dispatch(cycle());
        }, timeout);
      } else {
        await dispatch(cycle());
      }
    }
  };
}

export function updatePrice(player, settings) {
  return async dispatch => {
    const price = _.get(player, 'price', {});
    if (settings.autoUpdate) {
      const lastUpdated = moment(price.updated || 0);
      if (!price.buy || moment().isAfter(lastUpdated.add(1, 'h'))) {
        dispatch(addMessage('log', `Updating price for ${player.name}...`));
        await dispatch(findPrice(player.id));
      }
    } else {
      dispatch(addMessage('warn', `Auto update prices is disabled for ${player.name}...`));
    }
  };
}


export function getTradepile(email) {
  return async dispatch => {
    dispatch(addMessage('log', 'Updating tradepile...'));
    const api = getApi(email);
    const response = await api.getTradepile();
    // auctionInfo is not always an empty array
    response.auctionInfo = response.auctionInfo || [];
    dispatch(setCredits(response.credits));
    dispatch({ type: types.SET_TRADEPILE, tradepile: response.auctionInfo });
  };
}

export function getWatchlist(email) {
  return async dispatch => {
    dispatch(addMessage('log', 'Updating watchlist...'));
    const api = getApi(email);
    const response = await api.getWatchlist();
    // auctionInfo is not always an empty array
    response.auctionInfo = response.auctionInfo || [];
    dispatch(setCredits(response.credits));
    dispatch(setWatchlist(response.auctionInfo));
  };
}

export function setWatchlist(watchlist) {
  return { type: types.SET_WATCHLIST, watchlist };
}

export function getUnassigned(email) {
  return async dispatch => {
    dispatch(addMessage('log', 'Updating unassigned...'));
    const api = getApi(email);
    const response = await api.getUnassigned();
    dispatch(setCredits(response.credits));
    dispatch({ type: types.SET_UNASSIGNED, unassigned: response.itemData });
  };
}

export function updateHistory(id, history) {
  return { type: types.UPDATE_PLAYER_HISTORY, id, history };
}

export function stop() {
  if (biddingTimeout) {
    window.clearTimeout(biddingTimeout);
    biddingTimeout = undefined;
  }
  if (cycleTimeout) {
    window.clearTimeout(cycleTimeout);
    cycleTimeout = undefined;
  }
  return { type: types.STOP_BIDDING };
}

export function getMarketData(platform, type = 'live_graph', playerId = null) {
  return async dispatch => {
    /* istanbul ignore if */
    if (marketRequest) {
      marketRequest.abort();
      marketRequest = null;
    }
    return new Promise((resolve, reject) => {
      let url = 'https://www.futbin.com/pages/market/graph.php?';
      let qs = { type, console: platform.toUpperCase(), _: moment.utc().valueOf() };
      if (playerId) {
        url = 'https://www.futbin.com/pages/player/graph.php?';
        qs = { type, year: 17, player: playerId, _: moment.utc().valueOf() };
      }
      marketRequest = request.get(
        { url, qs },
        (error, response, body) => {
          if (error) {
            reject(error);
          }
          const json = JSON.parse(body);
          if (response.statusCode === 200) {
            resolve(json);
          }
        }
      );
    }).then(results => {
      const market = { title: results.title, flags: results.flags };
      switch (platform) {
        case 'xone':
        case 'x360':
          market.data = results.xbox;
          break;
        default:
          market.data = results.ps;
          break;
      }
      // Map flags
      const fillFlagColor = function fillFlagColor(design) {
        switch (parseInt(design, 10)) {
          case 1:
            return ['#000', '#000', '#dcb20a'];
          case 2:
            return ['#046aaf', '#046aaf', '#fff'];
          case 3:
            return ['#00591b', '#00591b', '#fff'];
          case 4:
            return ['#6099e6', '#6099e6', '#fff'];
          case 5:
            return ['#4f3581', '#4f3581', '#fff'];
          default:
            return ['#fff', '#000', '#fff'];
        }
      };
      market.flags = market.flags.map(flag => {
        const colorArray = fillFlagColor(flag.design);
        return {
          x: moment.utc(flag.flag_date).valueOf(),
          title: flag.title,
          text: flag.description,
          color: colorArray[0],
          fillColor: colorArray[1],
          style: {
            color: colorArray[2],
            borderRadius: 5
          }
        };
      });
      dispatch({ type: types.SAVE_MARKET_DATA, market });
    }).catch(() => { /* then we just won't update */ });
  };
}
