import utils from '../../utils';
import networkSvc from '../../networkSvc';
import store from '../../../store';
import userSvc from '../../userSvc';
import badgeSvc from '../../badgeSvc';

const appsDomain = null;
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h)

const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
const getDriveScopes = token => [token.driveFullAccess
  ? 'https://www.googleapis.com/auth/drive'
  : 'https://www.googleapis.com/auth/drive.file'];
const bloggerScopes = ['https://www.googleapis.com/auth/blogger'];
const photosScopes = ['https://www.googleapis.com/auth/photos'];

const checkIdToken = (idToken) => {
  try {
    const token = idToken.split('.');
    const payload = JSON.parse(utils.decodeBase64(token[1]));
    const clientId = store.getters['data/serverConf'].googleClientId;
    return payload.aud === clientId && Date.now() + tokenExpirationMargin < payload.exp * 1000;
  } catch (e) {
    return false;
  }
};

let driveState;
if (utils.queryParams.providerId === 'googleDrive') {
  try {
    driveState = JSON.parse(utils.queryParams.state);
  } catch (e) {
    // Ignore
  }
}

/**
 * https://developers.google.com/people/api/rest/v1/people/get
 */
const getUser = async (sub, token) => {
  const apiKey = store.getters['data/serverConf'].googleApiKey;
  const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos&key=${apiKey}`;
  const { body } = await networkSvc.request(sub === 'me' && token
    ? {
      method: 'GET',
      url,
      headers: {
        Authorization: `Bearer ${token.accessToken}`,
      },
    }
    : {
      method: 'GET',
      url,
    }, true);
  return body;
};

const subPrefix = 'go';
userSvc.setInfoResolver('google', subPrefix, async (sub) => {
  try {
    const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
    const body = await getUser(sub, googleToken);
    const name = (body.names && body.names[0]) || {};
    const photo = (body.photos && body.photos[0]) || {};
    return {
      id: `${subPrefix}:${sub}`,
      name: name.displayName,
      imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
    };
  } catch (err) {
    if (err.status !== 404) {
      throw new Error('RETRY');
    }
    throw err;
  }
});

export default {
  subPrefix,
  folderMimeType: 'application/vnd.google-apps.folder',
  driveState,
  driveActionFolder: null,
  driveActionFiles: [],
  async $request(token, options) {
    try {
      return (await networkSvc.request({
        ...options,
        headers: {
          ...options.headers || {},
          Authorization: `Bearer ${token.accessToken}`,
        },
      }, true)).body;
    } catch (err) {
      const { reason } = (((err.body || {}).error || {}).errors || [])[0] || {};
      if (reason === 'authError') {
        // Mark the token as revoked and get a new one
        store.dispatch('data/addGoogleToken', {
          ...token,
          expiresOn: 0,
        });
        // Refresh token and retry
        const refreshedToken = await this.refreshToken(token, token.scopes);
        return this.$request(refreshedToken, options);
      }
      throw err;
    }
  },

  /**
   * https://developers.google.com/identity/protocols/OpenIDConnect
   */
  async startOauth2(scopes, sub = null, silent = false) {
    await networkSvc.getServerConf();
    const clientId = store.getters['data/serverConf'].googleClientId;

    // Get an OAuth2 code
    const { accessToken, expiresIn, idToken } = await networkSvc.startOauth2(
      'https://accounts.google.com/o/oauth2/v2/auth',
      {
        client_id: clientId,
        response_type: 'token id_token',
        scope: ['openid', 'profile', ...scopes].join(' '),
        hd: appsDomain,
        login_hint: sub,
        prompt: silent ? 'none' : null,
        nonce: utils.uid(),
      },
      silent,
    );

    // Call the token info endpoint
    const { body } = await networkSvc.request({
      method: 'POST',
      url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
      params: {
        access_token: accessToken,
      },
    }, true);

    // Check the returned client ID consistency
    if (body.aud !== clientId) {
      throw new Error('Client ID inconsistent.');
    }
    // Check the returned sub consistency
    if (sub && `${body.sub}` !== sub) {
      throw new Error('Google account ID not expected.');
    }

    // Build token object including scopes and sub
    const existingToken = store.getters['data/googleTokensBySub'][body.sub];
    const token = {
      scopes,
      accessToken,
      expiresOn: Date.now() + (expiresIn * 1000),
      idToken,
      sub: body.sub,
      name: (existingToken || {}).name || 'Someone',
      isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
        scopes.includes('https://www.googleapis.com/auth/drive.appdata'),
      isSponsor: false,
      isDrive: scopes.includes('https://www.googleapis.com/auth/drive') ||
        scopes.includes('https://www.googleapis.com/auth/drive.file'),
      isBlogger: scopes.includes('https://www.googleapis.com/auth/blogger'),
      isPhotos: scopes.includes('https://www.googleapis.com/auth/photos'),
      driveFullAccess: scopes.includes('https://www.googleapis.com/auth/drive'),
    };

    // Call the user info endpoint
    const user = await getUser('me', token);
    const userId = user.resourceName.split('/')[1];
    const name = user.names[0] || {};
    const photo = user.photos[0] || {};
    if (name.displayName) {
      token.name = name.displayName;
    }
    userSvc.addUserInfo({
      id: `${subPrefix}:${userId}`,
      name: name.displayName,
      imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
    });

    if (existingToken) {
      // We probably retrieved a new token with restricted scopes.
      // That's no problem, token will be refreshed later with merged scopes.
      // Restore flags
      Object.assign(token, {
        isLogin: existingToken.isLogin || token.isLogin,
        isSponsor: existingToken.isSponsor,
        isDrive: existingToken.isDrive || token.isDrive,
        isBlogger: existingToken.isBlogger || token.isBlogger,
        isPhotos: existingToken.isPhotos || token.isPhotos,
        driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
      });
    }

    if (token.isLogin) {
      try {
        const res = await networkSvc.request({
          method: 'GET',
          url: 'userInfo',
          params: {
            idToken: token.idToken,
          },
        });
        token.isSponsor = res.body.sponsorUntil > Date.now();
        if (token.isSponsor) {
          badgeSvc.addBadge('sponsor');
        }
      } catch (err) {
        // Ignore
      }
    }

    // Add token to google tokens
    await store.dispatch('data/addGoogleToken', token);
    return token;
  },
  async refreshToken(token, scopes = []) {
    const { sub } = token;
    const lastToken = store.getters['data/googleTokensBySub'][sub];
    const mergedScopes = [...new Set([
      ...scopes,
      ...lastToken.scopes,
    ])];

    if (
      // If we already have permissions for the requested scopes
      mergedScopes.length === lastToken.scopes.length &&
      // And lastToken is not expired
      lastToken.expiresOn > Date.now() + tokenExpirationMargin &&
      // And in case of a login token, ID token is still valid
      (!lastToken.isLogin || checkIdToken(lastToken.idToken))
    ) {
      return lastToken;
    }

    // New scopes are requested or existing token is about to expire.
    // Try to get a new token in background
    try {
      return await this.startOauth2(mergedScopes, sub, true);
    } catch (err) {
      // If it fails try to popup a window
      if (store.state.offline) {
        throw err;
      }
      await store.dispatch('modal/open', {
        type: 'providerRedirection',
        name: 'Google',
      });
      return this.startOauth2(mergedScopes, sub);
    }
  },
  signin() {
    return this.startOauth2(driveAppDataScopes);
  },
  async addDriveAccount(fullAccess = false, sub = null) {
    const token = await this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
    badgeSvc.addBadge('addGoogleDriveAccount');
    return token;
  },
  async addBloggerAccount() {
    const token = await this.startOauth2(bloggerScopes);
    badgeSvc.addBadge('addBloggerAccount');
    return token;
  },
  async addPhotosAccount() {
    const token = await this.startOauth2(photosScopes);
    badgeSvc.addBadge('addGooglePhotosAccount');
    return token;
  },

  /**
   * https://developers.google.com/drive/v3/reference/files/create
   * https://developers.google.com/drive/v3/reference/files/update
   * https://developers.google.com/drive/v3/web/simple-upload
   */
  async $uploadFile({
    refreshedToken,
    name,
    parents,
    appProperties,
    media = null,
    mediaType = null,
    fileId = null,
    oldParents = null,
    ifNotTooLate = cb => cb(),
  }) {
    // Refreshing a token can take a while if an oauth window pops up, make sure it's not too late
    return ifNotTooLate(() => {
      const options = {
        method: 'POST',
        url: 'https://www.googleapis.com/drive/v3/files',
      };
      const params = {
        supportsTeamDrives: true,
      };
      const metadata = { name, appProperties };
      if (fileId) {
        options.method = 'PATCH';
        options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
        if (parents && oldParents) {
          params.addParents = parents
            .filter(parent => !oldParents.includes(parent))
            .join(',');
          params.removeParents = oldParents
            .filter(parent => !parents.includes(parent))
            .join(',');
        }
      } else if (parents) {
        metadata.parents = parents;
      }
      if (media) {
        const boundary = `-------${utils.uid()}`;
        const delimiter = `\r\n--${boundary}\r\n`;
        const closeDelimiter = `\r\n--${boundary}--`;
        let multipartRequestBody = '';
        multipartRequestBody += delimiter;
        multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
        multipartRequestBody += JSON.stringify(metadata);
        multipartRequestBody += delimiter;
        multipartRequestBody += `Content-Type: ${mediaType || 'text/plain'}; charset=UTF-8\r\n\r\n`;
        multipartRequestBody += media;
        multipartRequestBody += closeDelimiter;
        options.url = options.url.replace(
          'https://www.googleapis.com/',
          'https://www.googleapis.com/upload/',
        );
        return this.$request(refreshedToken, {
          ...options,
          params: {
            ...params,
            uploadType: 'multipart',
          },
          headers: {
            'Content-Type': `multipart/mixed; boundary="${boundary}"`,
          },
          body: multipartRequestBody,
        });
      }
      if (mediaType) {
        metadata.mimeType = mediaType;
      }
      return this.$request(refreshedToken, {
        ...options,
        body: metadata,
        params,
      });
    });
  },
  async uploadFile({
    token,
    name,
    parents,
    appProperties,
    media,
    mediaType,
    fileId,
    oldParents,
    ifNotTooLate,
  }) {
    const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
    return this.$uploadFile({
      refreshedToken,
      name,
      parents,
      appProperties,
      media,
      mediaType,
      fileId,
      oldParents,
      ifNotTooLate,
    });
  },
  async uploadAppDataFile({
    token,
    name,
    media,
    fileId,
    ifNotTooLate,
  }) {
    const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
    return this.$uploadFile({
      refreshedToken,
      name,
      parents: ['appDataFolder'],
      media,
      fileId,
      ifNotTooLate,
    });
  },

  /**
   * https://developers.google.com/drive/v3/reference/files/get
   */
  async getFile(token, id) {
    const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
    return this.$request(refreshedToken, {
      method: 'GET',
      url: `https://www.googleapis.com/drive/v3/files/${id}`,
      params: {
        fields: 'id,name,mimeType,appProperties,teamDriveId',
        supportsTeamDrives: true,
      },
    });
  },

  /**
   * https://developers.google.com/drive/v3/web/manage-downloads
   */
  async $downloadFile(refreshedToken, id) {
    return this.$request(refreshedToken, {
      method: 'GET',
      url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`,
      raw: true,
    });
  },
  async downloadFile(token, id) {
    const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
    return this.$downloadFile(refreshedToken, id);
  },
  async downloadAppDataFile(token, id) {
    const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
    return this.$downloadFile(refreshedToken, id);
  },

  /**
   * https://developers.google.com/drive/v3/reference/files/delete
   */
  async $removeFile(refreshedToken, id, ifNotTooLate = cb => cb()) {
    // Refreshing a token can take a while if an oauth window pops up, so check if it's too late
    return ifNotTooLate(() => this.$request(refreshedToken, {
      method: 'DELETE',
      url: `https://www.googleapis.com/drive/v3/files/${id}`,
      params: {
        supportsTeamDrives: true,
      },
    }));
  },
  async removeFile(token, id, ifNotTooLate) {
    const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
    return this.$removeFile(refreshedToken, id, ifNotTooLate);
  },
  async removeAppDataFile(token, id, ifNotTooLate = cb => cb()) {
    const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
    return this.$removeFile(refreshedToken, id, ifNotTooLate);
  },

  /**
   * https://developers.google.com/drive/v3/reference/revisions/list
   */
  async $getFileRevisions(refreshedToken, id) {
    const allRevisions = [];
    const getPage = async (pageToken) => {
      const { revisions, nextPageToken } = await this.$request(refreshedToken, {
        method: 'GET',
        url: `https://www.googleapis.com/drive/v3/files/${id}/revisions`,
        params: {
          pageToken,
          pageSize: 1000,
          fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
        },
      });
      revisions.forEach((revision) => {
        userSvc.addUserInfo({
          id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`,
          name: revision.lastModifyingUser.displayName,
          imageUrl: revision.lastModifyingUser.photoLink || '',
        });
        allRevisions.push(revision);
      });
      if (nextPageToken) {
        return getPage(nextPageToken);
      }
      return allRevisions;
    };
    return getPage();
  },
  async getFileRevisions(token, id) {
    const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
    return this.$getFileRevisions(refreshedToken, id);
  },
  async getAppDataFileRevisions(token, id) {
    const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
    return this.$getFileRevisions(refreshedToken, id);
  },

  /**
   * https://developers.google.com/drive/v3/reference/revisions/get
   */
  async $downloadFileRevision(refreshedToken, id, revisionId) {
    return this.$request(refreshedToken, {
      method: 'GET',
      url: `https://www.googleapis.com/drive/v3/files/${id}/revisions/${revisionId}?alt=media`,
      raw: true,
    });
  },
  async downloadFileRevision(token, fileId, revisionId) {
    const refreshedToken = await this.refreshToken(token, getDriveScopes(token));
    return this.$downloadFileRevision(refreshedToken, fileId, revisionId);
  },
  async downloadAppDataFileRevision(token, fileId, revisionId) {
    const refreshedToken = await this.refreshToken(token, driveAppDataScopes);
    return this.$downloadFileRevision(refreshedToken, fileId, revisionId);
  },

  /**
   * https://developers.google.com/drive/v3/reference/changes/list
   */
  async getChanges(token, startPageToken, isAppData, teamDriveId = null) {
    const result = {
      changes: [],
    };
    let fileFields = 'file/name';
    if (!isAppData) {
      fileFields += ',file/parents,file/mimeType,file/appProperties';
    }
    const refreshedToken = await this.refreshToken(
      token,
      isAppData ? driveAppDataScopes : getDriveScopes(token),
    );

    const getPage = async (pageToken = '1') => {
      const { changes, nextPageToken, newStartPageToken } = await this.$request(refreshedToken, {
        method: 'GET',
        url: 'https://www.googleapis.com/drive/v3/changes',
        params: {
          pageToken,
          spaces: isAppData ? 'appDataFolder' : 'drive',
          pageSize: 1000,
          fields: `nextPageToken,newStartPageToken,changes(fileId,${fileFields})`,
          supportsTeamDrives: true,
          includeTeamDriveItems: !!teamDriveId,
          teamDriveId,
        },
      });
      result.changes = [...result.changes, ...changes.filter(item => item.fileId)];
      if (nextPageToken) {
        return getPage(nextPageToken);
      }
      result.startPageToken = newStartPageToken;
      return result;
    };
    return getPage(startPageToken);
  },

  /**
   * https://developers.google.com/blogger/docs/3.0/reference/blogs/getByUrl
   * https://developers.google.com/blogger/docs/3.0/reference/posts/insert
   * https://developers.google.com/blogger/docs/3.0/reference/posts/update
   */
  async uploadBlogger({
    token,
    blogUrl,
    blogId,
    postId,
    title,
    content,
    labels,
    isDraft,
    published,
    isPage,
  }) {
    const refreshedToken = await this.refreshToken(token, bloggerScopes);

    // Get the blog ID
    const blog = { id: blogId };
    if (!blog.id) {
      blog.id = (await this.$request(refreshedToken, {
        url: 'https://www.googleapis.com/blogger/v3/blogs/byurl',
        params: {
          url: blogUrl,
        },
      })).id;
    }

    // Create/update the post/page
    const path = isPage ? 'pages' : 'posts';
    let options = {
      method: 'POST',
      url: `https://www.googleapis.com/blogger/v3/blogs/${blog.id}/${path}/`,
      body: {
        kind: isPage ? 'blogger#page' : 'blogger#post',
        blog,
        title,
        content,
      },
    };
    if (labels) {
      options.body.labels = labels;
    }
    if (published) {
      options.body.published = published.toISOString();
    }
    // If it's an update
    if (postId) {
      options.method = 'PUT';
      options.url += postId;
      options.body.id = postId;
    }
    const post = await this.$request(refreshedToken, options);
    if (isPage) {
      return post;
    }

    // Revert/publish post
    options = {
      method: 'POST',
      url: `https://www.googleapis.com/blogger/v3/blogs/${post.blog.id}/posts/${post.id}/`,
      params: {},
    };
    if (isDraft) {
      options.url += 'revert';
    } else {
      options.url += 'publish';
      if (published) {
        options.params.publishDate = published.toISOString();
      }
    }
    return this.$request(refreshedToken, options);
  },

  /**
   * https://developers.google.com/picker/docs/
   */
  async openPicker(token, type = 'doc') {
    const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
    if (!window.google) {
      await networkSvc.loadScript('https://apis.google.com/js/api.js');
      await new Promise((resolve, reject) => window.gapi.load('picker', {
        callback: resolve,
        onerror: reject,
        timeout: 30000,
        ontimeout: reject,
      }));
    }
    const refreshedToken = await this.refreshToken(token, scopes);
    const { google } = window;
    return new Promise((resolve) => {
      let picker;
      const pickerBuilder = new google.picker.PickerBuilder()
        .setOAuthToken(refreshedToken.accessToken)
        .enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES)
        .hideTitleBar()
        .setCallback((data) => {
          switch (data[google.picker.Response.ACTION]) {
            case google.picker.Action.PICKED:
            case google.picker.Action.CANCEL:
              resolve(data.docs || []);
              picker.dispose();
              break;
            default:
          }
        });
      switch (type) {
        default:
        case 'doc': {
          const mimeTypes = [
            'text/plain',
            'text/x-markdown',
            'application/octet-stream',
          ].join(',');

          const view = new google.picker.DocsView(google.picker.ViewId.DOCS);
          view.setMimeTypes(mimeTypes);
          pickerBuilder.addView(view);

          const teamDriveView = new google.picker.DocsView(google.picker.ViewId.DOCS);
          teamDriveView.setMimeTypes(mimeTypes);
          teamDriveView.setEnableTeamDrives(true);
          pickerBuilder.addView(teamDriveView);

          pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
          pickerBuilder.enableFeature(google.picker.Feature.SUPPORT_TEAM_DRIVES);
          break;
        }
        case 'folder': {
          const folderView = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
          folderView.setSelectFolderEnabled(true);
          folderView.setMimeTypes(this.folderMimeType);
          pickerBuilder.addView(folderView);

          const teamDriveView = new google.picker.DocsView(google.picker.ViewId.FOLDERS);
          teamDriveView.setSelectFolderEnabled(true);
          teamDriveView.setEnableTeamDrives(true);
          teamDriveView.setMimeTypes(this.folderMimeType);
          pickerBuilder.addView(teamDriveView);
          break;
        }
        case 'img': {
          const view = new google.picker.PhotosView();
          view.setType('highlights');
          pickerBuilder.addView(view);
          pickerBuilder.addView(google.picker.ViewId.PHOTO_UPLOAD);
          break;
        }
      }
      picker = pickerBuilder.build();
      picker.setVisible(true);
    });
  },
};
