/*
 * Copyright (c) 2018 by Filestack
 * Some rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { EventEmitter } from 'eventemitter3';
import * as Sentry from '@sentry/browser';
import { config, Hosts } from '../config';
import { FilestackError } from './../filestack_error';
import { metadata, MetadataOptions, remove, retrieve, RetrieveOptions, download } from './api/file';
import { transform, TransformOptions } from './api/transform';
import { storeURL } from './api/store';
import * as Utils from './utils';
import { Upload, InputFile, UploadOptions, StoreUploadOptions, UploadTags } from './api/upload';
import { preview, PreviewOptions } from './api/preview';
import { CloudClient } from './api/cloud';
import { Prefetch, PrefetchResponse, PrefetchOptions } from './api/prefetch';
import { FsResponse } from './request/types';
import { StoreParams } from './filelink';

import { picker, PickerInstance, PickerOptions } from './picker';

/* istanbul ignore next */
Sentry.addBreadcrumb({ category: 'sdk', message: 'filestack-js-sdk scope' });

export interface Session {
  apikey: string;
  urls: Hosts;
  cname?: string;
  policy?: string;
  signature?: string;
  prefetch?: PrefetchResponse;
}

export interface Security {
  policy: string;
  signature: string;
}

export interface ClientOptions {
  [option: string]: any;
  /**
   * Security object with policy and signature keys.
   * Can be used to limit client capabilities and protect public URLs.
   * It is intended to be used with server-side policy and signature generation.
   * Read about [security policies](https://www.filestack.com/docs/concepts/security).
   */
  security?: Security;
  /**
   * Domain to use for all URLs. __Requires the custom CNAME addon__.
   * If this is enabled then you must also set up your own OAuth applications
   * for each cloud source you wish to use in the picker.
   */
  cname?: string;
  /**
   * Enable/disable caching of the cloud session token. Default is false.
   * This ensures that users will be remembered on your domain when calling the cloud API from the browser.
   * Please be aware that tokens stored in localStorage are accessible by other scripts on the same domain.
   */
  sessionCache?: boolean;

  /**
   * Enable forwarding error logs to sentry
   * @default false
   */
  forwardErrors?: boolean;
}

/**
 * The Filestack client, the entry point for all public methods. Encapsulates session information.
 *
 * ### Example
 * ```js
 * // ES module
 * import * as filestack from 'filestack-js';
 * const client = filestack.init('apikey');
 * ```
 *
 * ```js
 * // UMD module in browser
 * <script src="https://static.filestackapi.com/filestack-js/3.x.x/filestack.min.js"></script>
 * const client = filestack.init('apikey');
 * ```
 */
export class Client extends EventEmitter {
  public session: Session;
  private cloud: CloudClient;
  private prefetchInstance: Prefetch;

  private forwardErrors: boolean = true;

  /**
   * Returns filestack utils
   *
   * @readonly
   * @memberof Client
   */
  get utils() {
    return Utils;
  }

  constructor(apikey: string, private options?: ClientOptions) {
    super();

    /* istanbul ignore if */
    if (options && options.forwardErrors) {
      this.forwardErrors = options.forwardErrors;
    }

    if (!apikey || typeof apikey !== 'string' || apikey.length === 0) {
      throw new Error('An apikey is required to initialize the Filestack client');
    }
    const { urls } = config;
    this.session = { apikey, urls };

    if (options) {
      const { cname, security } = options;

      this.setSecurity(security);
      this.setCname(cname);
    }

    this.prefetchInstance = new Prefetch(this.session);
    this.cloud = new CloudClient(this.session, options);
  }

  /**
   * Make basic prefetch request to check permissions
   *
   * @param params
   */
  prefetch(params: PrefetchOptions) {
    return this.prefetchInstance.getConfig(params);
  }

  /**
   * Set security object
   *
   * @param {Security} security
   * @memberof Client
   */
  setSecurity(security: Security) {
    if (security && !(security.policy && security.signature)) {
      throw new FilestackError('Both policy and signature are required for client security');
    }

    if (security && security.policy && security.signature) {
      this.session.policy = security.policy;
      this.session.signature = security.signature;
    }
  }

  /**
   * Set custom cname
   *
   * @param {string} cname
   * @returns
   * @memberof Client
   */
  setCname(cname: string) {
    if (!cname || cname.length === 0) {
      return;
    }

    this.session.cname = cname;
    this.session.urls = Utils.resolveHost(this.session.urls, cname);
  }

  /**
   * Clear all current cloud sessions in the picker.
   * Optionally pass a cloud source name to only log out of that cloud source.
   * This essentially clears the OAuth authorization codes from the Filestack session.
   * @param name Optional cloud source name.
   */
  logout(name?: string) {
    return this.cloud.logout(name);
  }
  /**
   * Retrieve detailed data of stored files.
   *
   * ### Example
   *
   * ```js
   * client
   *   .metadata('DCL5K46FS3OIxb5iuKby')
   *   .then((res) => {
   *     console.log(res);
   *   })
   *   .catch((err) => {
   *     console.log(err);
   *   }));
   * ```
   * @see [File API - Metadata](https://www.filestack.com/docs/api/file#metadata).
   * @param handle Valid Filestack handle.
   * @param options Metadata fields to enable on response.
   * @param security Optional security override.
   */
  metadata(handle: string, options?: MetadataOptions, security?: Security) {
    /* istanbul ignore next */
    return metadata(this.session, handle, options, security);
  }
  /**
   * Construct a new picker instance.
   */
  picker(options?: PickerOptions): PickerInstance {
    /* istanbul ignore next */
    return picker(this, options);
  }
  /**
   * Used for viewing files via Filestack handles or storage aliases, __requires Document Viewer addon to your Filestack application__.
   * Opens document viewer in new window if id option is not provided.
   *
   * ### Example
   *
   * ```js
   * // <div id="preview"></div>
   *
   * client.preview('DCL5K46FS3OIxb5iuKby', { id: 'preview' });
   * ```
   * @param handle Valid Filestack handle.
   * @param options Preview options
   */
  preview(handle: string, options?: PreviewOptions) {
    /* istanbul ignore next */
    return preview(this.session, handle, options);
  }
  /**
   * Remove a file from storage and the Filestack system.
   *
   * __Requires a valid security policy and signature__. The policy and signature will be pulled from the client session, or it can be overridden with the security parameter.
   *
   * ### Example
   *
   * ```js
   * client
   *   .remove('DCL5K46FS3OIxb5iuKby')
   *   .then((res) => {
   *     console.log(res);
   *   })
   *   .catch((err) => {
   *     console.log(err);
   *   }));
   * ```
   * @see [File API - Delete](https://www.filestack.com/docs/api/file#delete)
   * @param handle Valid Filestack handle.
   * @param security Optional security override.
   */
  remove(handle: string, security?: Security): Promise<any> {
    /* istanbul ignore next */
    return remove(this.session, handle, false, security);
  }
  /**
   * Remove a file **only** from the Filestack system. The file remains in storage.
   *
   * __Requires a valid security policy and signature__. The policy and signature will be pulled from the client session, or it can be overridden with the security parameter.
   *
   * ### Example
   *
   * ```js
   * client
   *   .removeMetadata('DCL5K46FS3OIxb5iuKby')
   *   .then((res) => {
   *     console.log(res);
   *   })
   *   .catch((err) => {
   *     console.log(err);
   *   }));
   * ```
   * @see [File API - Delete](https://www.filestack.com/docs/api/file#delete)
   * @param handle Valid Filestack handle.
   * @param security Optional security override.
   */
  removeMetadata(handle: string, security?: Security): Promise<any> {
    /* istanbul ignore next */
    return remove(this.session, handle, true, security);
  }
  /**
   * Store a file from its URL.
   *
   * ### Example
   *
   * ```js
   * client
   *   .storeURL('https://d1wtqaffaaj63z.cloudfront.net/images/NY_199_E_of_Hammertown_2014.jpg')
   *   .then(res => console.log(res));
   * ```
   * @see [File API - Store](https://www.filestack.com/docs/api/file#store)
   * @param url       Valid URL to a file.
   * @param options   Configure file storage.
   * @param token     Optional control token to call .cancel()
   * @param security  Optional security override.
   * @param uploadTags Optional tags visible in webhooks.
   * @param headers    Optional headers to send
   * @param workflowIds    Optional workflowIds to send
   */
  storeURL(url: string, storeParams?: StoreParams, token?: any, security?: Security, uploadTags?: UploadTags, headers?: {[key: string]: string}, workflowIds?: string[]): Promise<Object> {
    return storeURL({
      session: this.session,
      url,
      storeParams,
      token,
      security,
      uploadTags,
      headers,
      workflowIds,
    });
  }

  /**
   * Access files via their Filestack handles.
   *
   * If head option is provided - request headers are returned in promise
   * If metadata option is provided - metadata object is returned in promise
   * Otherwise file blob is returned
   * Metadata and head options cannot be mixed
   *
   * ### Example
   *
   * ```js
   * client.retrieve('fileHandle', {
   *  metadata: true,
   * }).then((response) => {
   *  console.log(response);
   * }).catch((err) => {
   *  console.error(err);
   * })
   * ```
   *
   * @see [File API - Download](https://www.filestack.com/docs/api/file#download)
   * @deprecated use metadata or download methods instead
   * @param handle    Valid file handle
   * @param options   RetrieveOptions
   * @param security  Optional security override.
   * @throws          Error
   */
  retrieve(handle: string, options?: RetrieveOptions, security?: Security): Promise<Object | Blob> {
    /* istanbul ignore next */
    return retrieve(this.session, handle, options, security);
  }

  /**
   * Download file by handle
   *
   *
   * ### Browser Example
   *
   * ```js
   * client.download('fileHandle').then((response) => {
   * const img = new Image();
   * img.src = URL.createObjectURL(res.data)
   * document.body.appendChild(img);
   * }).catch((err) => {
   *  console.error(err);
   * })
   * ```
   *
   * @see [File API - Download](https://www.filestack.com/docs/api/file#download)
   * @param handle    Valid file handle
   * @throws          Error
   */
  download(handle: string, security?: Security): Promise<FsResponse> {
    /* istanbul ignore next */
    return download(this.session, handle, security);
  }

  /**
   * Interface to the Filestack [Processing API](https://www.filestack.com/docs/api/processing).
   * Convert a URL, handle, or storage alias to another URL which links to the transformed file.
   * You can optionally store the returned URL with client.storeURL.
   *
   * Transform params can be provided in camelCase or snakeCase style ie: partial_pixelate or partialPixelate
   *
   * ### Example
   *
   * ```js
   * const transformedUrl = client.transform(url, {
   *   crop: {
   *     dim: [x, y, width, height],
   *   },
   *   vignette: {
   *     blurmode: 'gaussian',
   *     amount: 50,
   *   },
   *   flip: true,
   *   partial_pixelate: {
   *     objects: [[10, 20, 200, 250], [275, 91, 500, 557]],
   *   },
   * };
   *
   * // optionally store the new URL
   * client.storeURL(transformedUrl).then(res => console.log(res));
   * ```
   * @see [Filestack Processing API](https://www.filestack.com/docs/api/processing)
   * @param url     Single or multiple valid URLs (http(s)://), file handles, or storage aliases (src://) to an image.
   * @param options Transformations are applied in the order specified by this object.
   * @param b64     Use new more safe format for generating transforms url (default=false) Note: If there will be any issues with url please test it with enabled b64 support
   * @returns       A new URL that points to the transformed resource.
   */
  transform(url: string | string[], options: TransformOptions, b64: boolean = false) {
    /* istanbul ignore next */
    return transform(this.session, url, options, b64);
  }

  /**
   * Initiates a multi-part upload flow. Use this for Filestack CIN and FII uploads.
   *
   * In Node runtimes the file argument is treated as a file path.
   * Uploading from a Node buffer is not yet implemented.
   *
   * ### Example
   *
   * ```js
   * const token = {};
   * const onRetry = (obj) => {
   *   console.log(`Retrying ${obj.location} for ${obj.filename}. Attempt ${obj.attempt} of 10.`);
   * };
   *
   * client.upload(file, { onRetry }, { filename: 'foobar.jpg' }, token)
   *   .then(res => console.log(res));
   *
   * client.upload({file, name}, { onRetry }, { filename: 'foobar.jpg' }, token)
   *   .then(res => console.log(res));
   *
   * token.pause();  // Pause flow
   * token.resume(); // Resume flow
   * token.cancel(); // Cancel flow (rejects)
   * ```
   * @param {InputFile}    file           Must be a valid [File | Blob | Buffer | string]
   * @param uploadOptions  Uploader options.
   * @param storeOptions   Storage options.
   * @param token          A control token that can be used to call cancel(), pause(), and resume().
   * @param security       Optional security policy and signature override.
   *
   * @returns {Promise}
   */
  upload(file: InputFile, options?: UploadOptions, storeOptions?: StoreUploadOptions, token?: any, security?: Security) {
    let upload = new Upload(options, storeOptions);
    upload.setSession(this.session);

    if (token) {
      upload.setToken(token);
    }

    if (security) {
      upload.setSecurity(security);
    }

    upload.on('start', () => this.emit('upload.start'));
    /* istanbul ignore next */
    upload.on('error', e => {
      if (this.forwardErrors) {
        Sentry.withScope(scope => {
          scope.setTag('filestack-apikey', this.session.apikey);
          scope.setTag('filestack-version', Utils.getVersion());
          scope.setExtra('filestack-options', this.options);
          scope.setExtras({ uploadOptions: options, storeOptions, details: e.details });
          e.message = `FS-${e.message}`;
          scope.captureException(e);
        });
      }

      this.emit('upload.error', e);
    });

    return upload.upload(file, options && options.altText);
  }

  /**
   * Initiates a multi-part upload flow. Use this for Filestack CIN and FII uploads.
   *
   * In Node runtimes the file argument is treated as a file path.
   * Uploading from a Node buffer is not yet implemented.
   *
   * ### Example
   *
   * ```js
   * const token = {};
   * const onRetry = (obj) => {
   *   console.log(`Retrying ${obj.location} for ${obj.filename}. Attempt ${obj.attempt} of 10.`);
   * };
   *
   * client.multiupload([file], { onRetry }, token)
   *   .then(res => console.log(res));
   *
   * client.multiupload([{file, name}], { onRetry }, token)
   *   .then(res => console.log(res));
   *
   * token.pause();  // Pause flow
   * token.resume(); // Resume flow
   * token.cancel(); // Cancel flow (rejects)
   * ```
   * @param {InputFile[]}  file           Must be a valid [File | Blob | Buffer | string (base64)]
   * @param uploadOptions  Upload options.
   * @param storeOptions   Storage options.
   * @param token          A control token that can be used to call cancel(), pause(), and resume().
   * @param security       Optional security policy and signature override.
   *
   * @returns {Promise}
   */
  multiupload(file: InputFile[], options?: UploadOptions, storeOptions?: StoreUploadOptions, token?: any, security?: Security) {
    let upload = new Upload(options, storeOptions);

    upload.setSession(this.session);

    if (token) {
      upload.setToken(token);
    }

    if (security) {
      upload.setSecurity(security);
    }

    upload.on('start', () => this.emit('upload.start'));
    /* istanbul ignore next */
    upload.on('error', e => {
      Sentry.withScope(scope => {
        scope.setTag('filestack-apikey', this.session.apikey);
        scope.setTag('filestack-version', Utils.getVersion());
        scope.setExtra('filestack-options', this.options);
        scope.setExtras(e.details);
        scope.setExtras({ uploadOptions: options, storeOptions });
        scope.captureException(e);
      });

      this.emit('upload.error', e);
    });

    return upload.multiupload(file);
  }
}
