const { Logger } = require('lib/logger.js');
const { shim } = require('lib/shim.js');
const JoplinError = require('lib/JoplinError');
const { time } = require('lib/time-utils');
const EventDispatcher = require('lib/EventDispatcher');

class DropboxApi {
	constructor(options) {
		this.logger_ = new Logger();
		this.options_ = options;
		this.authToken_ = null;
		this.dispatcher_ = new EventDispatcher();
	}

	clientId() {
		return this.options_.id;
	}

	clientSecret() {
		return this.options_.secret;
	}

	setLogger(l) {
		this.logger_ = l;
	}

	logger() {
		return this.logger_;
	}

	authToken() {
		return this.authToken_; // Without the "Bearer " prefix
	}

	on(eventName, callback) {
		return this.dispatcher_.on(eventName, callback);
	}

	setAuthToken(v) {
		this.authToken_ = v;
		this.dispatcher_.dispatch('authRefreshed', this.authToken());
	}

	loginUrl() {
		return `https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=${this.clientId()}`;
	}

	baseUrl(endPointFormat) {
		if (['content', 'api'].indexOf(endPointFormat) < 0) throw new Error(`Invalid end point format: ${endPointFormat}`);
		return `https://${endPointFormat}.dropboxapi.com/2`;
	}

	requestToCurl_(url, options) {
		const output = [];
		output.push('curl');
		if (options.method) output.push(`-X ${options.method}`);
		if (options.headers) {
			for (const n in options.headers) {
				if (!options.headers.hasOwnProperty(n)) continue;
				output.push(`${'-H ' + '\''}${n}: ${options.headers[n]}'`);
			}
		}
		if (options.body) output.push(`${'--data ' + '"'}${options.body}"`);
		output.push(url);

		return output.join(' ');
	}

	async execAuthToken(authCode) {
		const postData = {
			code: authCode,
			grant_type: 'authorization_code',
			client_id: this.clientId(),
			client_secret: this.clientSecret(),
		};

		let formBody = [];
		for (const property in postData) {
			const encodedKey = encodeURIComponent(property);
			const encodedValue = encodeURIComponent(postData[property]);
			formBody.push(`${encodedKey}=${encodedValue}`);
		}
		formBody = formBody.join('&');

		const response = await shim.fetch('https://api.dropboxapi.com/oauth2/token', {
			method: 'POST',
			headers: {
				'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
			},
			body: formBody,
		});

		const responseText = await response.text();
		if (!response.ok) throw new Error(responseText);
		return JSON.parse(responseText);
	}

	isTokenError(status, responseText) {
		if (status === 401) return true;
		if (responseText.indexOf('OAuth 2 access token is malformed') >= 0) return true;
		// eg. Error: POST files/create_folder_v2: Error (400): Error in call to API function "files/create_folder_v2": Must provide HTTP header "Authorization" or URL parameter "authorization".
		if (responseText.indexOf('Must provide HTTP header "Authorization"') >= 0) return true;
		return false;
	}

	async exec(method, path = '', body = null, headers = null, options = null) {
		if (headers === null) headers = {};
		if (options === null) options = {};
		if (!options.target) options.target = 'string';

		const authToken = this.authToken();

		if (authToken) headers['Authorization'] = `Bearer ${authToken}`;

		const endPointFormat = ['files/upload', 'files/download'].indexOf(path) >= 0 ? 'content' : 'api';

		if (endPointFormat === 'api') {
			headers['Content-Type'] = 'application/json';
			if (body && typeof body === 'object') body = JSON.stringify(body);
		} else {
			headers['Content-Type'] = 'application/octet-stream';
		}

		const fetchOptions = {};
		fetchOptions.headers = headers;
		fetchOptions.method = method;
		if (options.path) fetchOptions.path = options.path;
		if (body) fetchOptions.body = body;

		const url = path.indexOf('https://') === 0 ? path : `${this.baseUrl(endPointFormat)}/${path}`;

		let tryCount = 0;

		while (true) {
			try {
				let response = null;

				// console.info(this.requestToCurl_(url, fetchOptions));

				// console.info(method + ' ' + url);

				if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
					response = await shim.uploadBlob(url, fetchOptions);
				} else if (options.target == 'string') {
					response = await shim.fetch(url, fetchOptions);
				} else {
					// file
					response = await shim.fetchBlob(url, fetchOptions);
				}

				const responseText = await response.text();

				// console.info('Response: ' + responseText);

				let responseJson_ = null;
				const loadResponseJson = () => {
					if (!responseText) return null;
					if (responseJson_) return responseJson_;
					try {
						responseJson_ = JSON.parse(responseText);
					} catch (error) {
						return { error: responseText };
					}
					return responseJson_;
				};

				// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
				const newError = message => {
					const json = loadResponseJson();
					let code = '';
					if (json && json.error_summary) {
						code = json.error_summary;
					}

					// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
					// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
					const shortResponseText = (`${responseText}`).substr(0, 1024);
					const error = new JoplinError(`${method} ${path}: ${message} (${response.status}): ${shortResponseText}`, code);
					error.httpStatus = response.status;
					return error;
				};

				if (!response.ok) {
					if (this.isTokenError(response.status, responseText)) {
						this.setAuthToken(null);
					}

					// When using fetchBlob we only get a string (not xml or json) back
					if (options.target === 'file') throw newError('fetchBlob error');

					throw newError('Error');
				}

				if (options.responseFormat === 'text') return responseText;

				return loadResponseJson();
			} catch (error) {
				tryCount++;
				if (error && typeof error.code === 'string' && error.code.indexOf('too_many_write_operations') >= 0) {
					this.logger().warn(`too_many_write_operations ${tryCount}`);
					if (tryCount >= 3) {
						throw error;
					}
					await time.sleep(tryCount * 2);
				} else {
					throw error;
				}
			}
		}
	}
}

module.exports = DropboxApi;