import namedQueue from 'named-queue';
import * as options from './options.js';
import * as realdebrid from './realdebrid.js';
import * as premiumize from './premiumize.js';
import * as alldebrid from './alldebrid.js';
import * as debridlink from './debridlink.js';
import * as offcloud from './offcloud.js';
import * as putio from './putio.js';
import StaticResponse, { isStaticUrl } from './static.js';
import { cacheWrapResolvedUrl } from '../lib/cache.js';
import { timeout } from '../lib/promises.js';
import { BadTokenError, streamFilename, AccessDeniedError, enrichMeta } from './mochHelper.js';

const RESOLVE_TIMEOUT = 2 * 60 * 1000; // 2 minutes
const MIN_API_KEY_SYMBOLS = 15;
const TOKEN_BLACKLIST = [];
export const MochOptions = {
  realdebrid: {
    key: 'realdebrid',
    instance: realdebrid,
    name: "RealDebrid",
    shortName: 'RD',
    catalog: true
  },
  premiumize: {
    key: 'premiumize',
    instance: premiumize,
    name: 'Premiumize',
    shortName: 'PM',
    catalog: true
  },
  alldebrid: {
    key: 'alldebrid',
    instance: alldebrid,
    name: 'AllDebrid',
    shortName: 'AD',
    catalog: true
  },
  debridlink: {
    key: 'debridlink',
    instance: debridlink,
    name: 'DebridLink',
    shortName: 'DL',
    catalog: true
  },
  offcloud: {
    key: 'offcloud',
    instance: offcloud,
    name: 'Offcloud',
    shortName: 'OC',
    catalog: true
  },
  putio: {
    key: 'putio',
    instance: putio,
    name: 'Put.io',
    shortName: 'Putio',
    catalog: true
  }
};

const unrestrictQueues = {}
Object.values(MochOptions)
    .map(moch => moch.key)
    .forEach(mochKey => unrestrictQueues[mochKey] = new namedQueue((task, callback) => task.method()
      .then(result => callback(false, result))
      .catch((error => callback(error))), 200));

export function hasMochConfigured(config) {
  return Object.keys(MochOptions).find(moch => config?.[moch])
}

export async function applyMochs(streams, config) {
  if (!streams?.length || !hasMochConfigured(config)) {
    return streams;
  }
  return Promise.all(Object.keys(config)
      .filter(configKey => MochOptions[configKey])
      .map(configKey => MochOptions[configKey])
      .map(moch => {
        if (isInvalidToken(config[moch.key], moch.key)) {
          return { moch, error: BadTokenError };
        }
        return moch.instance.getCachedStreams(streams, config[moch.key])
            .then(mochStreams => ({ moch, mochStreams }))
            .catch(rawError => {
              const error = moch.instance.toCommonError(rawError) || rawError;
              if (error === BadTokenError) {
                blackListToken(config[moch.key], moch.key);
              }
              return { moch, error };
            })
      }))
      .then(results => processMochResults(streams, config, results));
}

export async function resolve(parameters) {
  const moch = MochOptions[parameters.mochKey];
  if (!moch) {
    return Promise.reject(new Error(`Not a valid moch provider: ${parameters.mochKey}`));
  }

  if (!parameters.apiKey || !parameters.infoHash || !parameters.cachedEntryInfo) {
    return Promise.reject(new Error("No valid parameters passed"));
  }
  const id = `${parameters.ip}_${parameters.mochKey}_${parameters.apiKey}_${parameters.infoHash}_${parameters.fileIndex}`;
  const method = () => timeout(RESOLVE_TIMEOUT, cacheWrapResolvedUrl(id, () => moch.instance.resolve(parameters)))
      .catch(error => {
        console.warn(error);
        return StaticResponse.FAILED_UNEXPECTED;
      })
      .then(url => isStaticUrl(url) ? `${parameters.host}/${url}` : url);
  const unrestrictQueue = unrestrictQueues[moch.key];
  return new Promise(((resolve, reject) => {
    unrestrictQueue.push({ id, method }, (error, result) => result ? resolve(result) : reject(error));
  }));
}

export async function getMochCatalog(mochKey, config) {
  const moch = MochOptions[mochKey];
  if (!moch) {
    return Promise.reject(new Error(`Not a valid moch provider: ${mochKey}`));
  }
  if (isInvalidToken(config[mochKey], mochKey)) {
    return Promise.reject(new Error(`Invalid API key for moch provider: ${mochKey}`));
  }
  return moch.instance.getCatalog(config[moch.key], config.skip, config.ip)
      .catch(rawError => {
        const commonError = moch.instance.toCommonError(rawError);
        if (commonError === BadTokenError) {
          blackListToken(config[moch.key], moch.key);
        }
        return commonError ? [] : Promise.reject(rawError);
      });
}

export async function getMochItemMeta(mochKey, itemId, config) {
  const moch = MochOptions[mochKey];
  if (!moch) {
    return Promise.reject(new Error(`Not a valid moch provider: ${mochKey}`));
  }

  return moch.instance.getItemMeta(itemId, config[moch.key], config.ip)
      .then(meta => enrichMeta(meta))
      .then(meta => {
        meta.videos.forEach(video => video.streams.forEach(stream => {
          if (!stream.url.startsWith('http')) {
            stream.url = `${config.host}/${moch.key}/${stream.url}/${streamFilename(video)}`
          }
          stream.behaviorHints = { bingeGroup: itemId }
        }))
        return meta;
      });
}

function processMochResults(streams, config, results) {
  const errorResults = results
      .map(result => errorStreamResponse(result.moch.key, result.error, config))
      .filter(errorResponse => errorResponse);
  if (errorResults.length) {
    return errorResults;
  }

  const includeTorrentLinks = options.includeTorrentLinks(config);
  const excludeDownloadLinks = options.excludeDownloadLinks(config);
  const mochResults = results.filter(result => result?.mochStreams);

  const cachedStreams = mochResults
      .reduce((resultStreams, mochResult) => populateCachedLinks(resultStreams, mochResult, config), streams);
  const resultStreams = excludeDownloadLinks ? cachedStreams : populateDownloadLinks(cachedStreams, mochResults, config);
  return includeTorrentLinks ? resultStreams : resultStreams.filter(stream => stream.url);
}

function populateCachedLinks(streams, mochResult, config) {
  return streams.map(stream => {
    const cachedEntry = stream.infoHash && mochResult.mochStreams[stream.infoHash];
    if (cachedEntry?.cached) {
      return {
        name: `[${mochResult.moch.shortName}+] ${stream.name}`,
        title: stream.title,
        url: `${config.host}/${mochResult.moch.key}/${cachedEntry.url}/${streamFilename(stream)}`,
        behaviorHints: stream.behaviorHints
      };
    }
    return stream;
  });
}

function populateDownloadLinks(streams, mochResults, config) {
  const torrentStreams = streams.filter(stream => stream.infoHash);
  const seededStreams = streams.filter(stream => !stream.title.includes('👤 0'));
  torrentStreams.forEach(stream => mochResults.forEach(mochResult => {
    const cachedEntry = mochResult.mochStreams[stream.infoHash];
    const isCached = cachedEntry?.cached;
    if (!isCached && isHealthyStreamForDebrid(seededStreams, stream)) {
      streams.push({
        name: `[${mochResult.moch.shortName} download] ${stream.name}`,
        title: stream.title,
        url: `${config.host}/${mochResult.moch.key}/${cachedEntry.url}/${streamFilename(stream)}`,
        behaviorHints: stream.behaviorHints
      })
    }
  }));
  return streams;
}

function isHealthyStreamForDebrid(streams, stream) {
  const isZeroSeeders = stream.title.includes('👤 0');
  const is4kStream = stream.name.includes('4k');
  const isNotEnoughOptions = streams.length <= 5;
  return !isZeroSeeders || is4kStream || isNotEnoughOptions;
}

function isInvalidToken(token, mochKey) {
  return token.length < MIN_API_KEY_SYMBOLS || TOKEN_BLACKLIST.includes(`${mochKey}|${token}`);
}

function blackListToken(token, mochKey) {
  const tokenKey = `${mochKey}|${token}`;
  console.log(`Blacklisting invalid token: ${tokenKey}`)
  TOKEN_BLACKLIST.push(tokenKey);
}

function errorStreamResponse(mochKey, error, config) {
  if (error === BadTokenError) {
    return {
      name: `Torrentio\n${MochOptions[mochKey].shortName} error`,
      title: `Invalid ${MochOptions[mochKey].name} ApiKey/Token!`,
      url: `${config.host}/${StaticResponse.FAILED_ACCESS}`
    };
  }
  if (error === AccessDeniedError) {
    return {
      name: `Torrentio\n${MochOptions[mochKey].shortName} error`,
      title: `Expired/invalid ${MochOptions[mochKey].name} subscription!`,
      url: `${config.host}/${StaticResponse.FAILED_ACCESS}`
    };
  }
  return undefined;
}
