import Emitter, { IEmitter } from './Emitter';
import _ from 'underscore';
import { IStorageService } from './Storage';
import { IApiService } from './Api';
import Promise from 'bluebird';
import UserData, { IUserDataService } from './UserData';

export interface IAuthService extends IEmitter {
  EVENT_LOGGED_IN: string;
  EVENT_RE_LOGGING: string;
  EVENT_LOGIN_REFRESHED: string;
  EVENT_LOGGED_OUT: string;
  EVENT_ERROR: string;
  EVENT_401: string;
  EVENT_USER_UPDATED: string;

  user: IUserSelf;

  authenticate: (force?: boolean) => Promise<any>;
  getUserInfo(): Promise<IUserSelf>;
  login: (username: string, password: string) => Promise<any>;
  logout: () => void;
}

export interface IUserSelfOrganization {
  allow_client_app_downloads: boolean;
  is_enterprise: boolean;
  is_paying_customer: boolean;
  stripe_plan_id: string;
  support_level: string;
}

export interface IUserSelf {
  id: number;
  url: string;
  email: string;
  screen_name: string;
  first_name: string;
  last_name: string;
  mobile_phone_number: string;
  pushbullet_id: string;
  default_domain_id: number;
  default_page_id: number;
  default_domain_name: string;
  default_page_name: string;
  pending_invitation_count: number;
  can_create_folders: boolean;
  meta_data: any[];
  organization_id: number;
  UserData: IUserDataService;
  organization: IUserSelfOrganization;
  invited_user?: any;
}

export class Auth extends Emitter {
  public static $inject: string[] = [
    '$q',
    '$timeout',
    'ippApiService',
    'ippStorageService',
    'ippConfig',
    'ippUtilsService'
  ];

  public get EVENT_LOGGED_IN(): string {
    return 'logged_in';
  } // emitted when logged in (not on login refresh)
  public get EVENT_RE_LOGGING(): string {
    return 're_logging';
  } // emitted when re-logging started
  public get EVENT_LOGIN_REFRESHED(): string {
    return 'login_refreshed';
  } // emitted only after 401 and re-login
  public get EVENT_LOGGED_OUT(): string {
    return 'logged_out';
  } // emitted when logged out
  public get EVENT_ERROR(): string {
    return 'error';
  }
  public get EVENT_401(): string {
    return '401';
  } // listener

  public get EVENT_USER_UPDATED(): string {
    return 'user_updated';
  }

  public polling: boolean = false;
  public UserData: IUserDataService;

  private _user: IUserSelf | any = {};
  private _authenticated: boolean = false;
  private _authInProgress: boolean = false;

  private _selfTimer: any;
  private _selfTimeout: number = 15 * 1000; // @todo Should not be hardcoded

  public get user(): IUserSelf {
    return this._user;
  }

  constructor(private config: any, private ippApi: IApiService, private storage: IStorageService) {
    super();
    this._selfTimeout = _.random(15, 45) * 1000;
    ippApi.on(ippApi.EVENT_401, this.on401);
    this.UserData = new UserData(ippApi, storage, 0);
  }

  public authenticate(force: boolean = false, tokens: any = {}): Promise<any> {
    if (tokens && tokens.access_token) {
      this.ippApi.tokens = tokens;
    }

    let p: Promise<any> = new Promise((resolve, reject) => {
      if (this._authInProgress) {
        reject(new Error('Auth already in progress')); // @todo or resolve?
        return;
      }

      this._authInProgress = true;

      if (this._authenticated && !force) {
        this._authInProgress = false;
        resolve(); // Actually at this moment we have no idea if access token is valid but we dont have to worry about that because of the 401 process from API
        return;
      }

      this.processAuth(force)
        .then(() => {
          if (!this._authenticated) {
            this._authenticated = true;
            this.UserData = new UserData(this.ippApi, this.storage, this.user.id);
            this.UserData.parse(this.user.meta_data);

            // @todo oh no....
            this.storage.user.suffix = this._user.id;

            this.emit(this.EVENT_LOGGED_IN);

            this.startPollingSelf();
          }

          resolve();
        })
        .catch(err => {
          this.emit(this.EVENT_ERROR, err);

          if (this._authenticated) {
            this.logout(false, true);
          }

          reject(new Error(err));
        })
        .finally(() => {
          this._authInProgress = false;
        });
    });

    return p;
  }

  public login(username: string, password: string): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      this.ippApi
        .userLogin({
          grant_type: 'password',
          client_id: this.config.api_key,
          client_secret: this.config.api_secret,
          email: username,
          password: password
        })
        .then(res => {
          this.saveTokens(res.data);
          return this.authenticate();
        })
        .then(() => {
          resolve();
        })
        .catch(err => {
          if (err.httpCode === 400 || err.httpCode === 401) {
            switch (err.data) {
              case 'invalid_grant':
                err.message =
                  'The username and password you entered did not match our records. Please double-check and try again.';
                break;
              case 'invalid_client':
                err.message = "Your client doesn't have access to iPushPull system.";
                break;
              case 'invalid_request':
                // err.message = err.message;
                break;
              default:
                // err.message = this.utils.parseApiError(err.data, "Unknown error");
                break;
            }
          }
          this.emit(this.EVENT_ERROR, err);
          reject(err);
        });
    });

    return p;
  }

  public logout(all: boolean = false, ignore: boolean = false): void {
    // Call rest endpoint to destroy tokens - don't need to wait for result
    if (!ignore) {
      this.ippApi.userLogout({ all: all });
    }

    this.ippApi.tokens = {
      access_token: '',
      refresh_token: ''
    };

    this.storage.persistent.remove('access_token');
    this.storage.persistent.remove('refresh_token');
    this.storage.persistent.remove('renew');

    this._authenticated = false;
    this._user = {};
    this.UserData.destroy();

    clearTimeout(this._selfTimer);

    // @todo oh no....
    this.storage.user.suffix = 'GUEST';

    this.emit(this.EVENT_LOGGED_OUT);
  }

  public isEnterprise(): boolean {
    return this._authenticated ? this.user.organization.is_enterprise : false;
  }

  private processAuth(force: boolean = false): Promise<any> {
    let p: Promise<any> = new Promise((resolve, reject) => {
      let accessToken: string = this.storage.persistent.get('access_token') || this.ippApi.tokens.access_token;
      let refreshToken: string = this.storage.persistent.get('refresh_token') || this.ippApi.tokens.refresh_token;

      if (accessToken && !force) {
        this.getUserInfo().then(resolve, reject);
      } else {
        if (refreshToken) {
          this.refreshTokens()
            .then(data => {
              this.saveTokens(data.data);
              this.getUserInfo().then(resolve, reject);
            })
            .catch(err => {
              this.storage.persistent.remove('refresh_token');
              reject(new Error(err));
            });
        } else {
          reject(new Error('No tokens available'));
        }
      }
    });

    return p;
  }

  private refreshTokens(): Promise<any> {
    let refreshToken: string = this.storage.persistent.get('refresh_token') || this.ippApi.tokens.refresh_token;
    return this.ippApi.refreshAccessTokens(refreshToken);
  }

  private saveTokens(tokens: any): void {
    this.storage.persistent.create('access_token', tokens.access_token, tokens.expires_in / 86400);
    this.storage.persistent.create('refresh_token', tokens.refresh_token, 365);
    this.ippApi.tokens = tokens;
    this.storage.persistent.remove('renew');
  }

  public getUserInfo(): Promise<IUserSelf> {
    let p: Promise<IUserSelf> = new Promise((resolve, reject) => {
      this.ippApi.getSelfInfo().then(res => {
        if (!_.isEqual(this._user, res.data)) {
          let updated = this._user.id;
          this._user = res.data; // going outside function scope..
          this.storage.user.suffix = this._user.id;
          this.UserData.parse(this.user.meta_data);
          if (updated) this.emit(this.EVENT_USER_UPDATED);
        }
        resolve();
      }, reject);
    });

    return p;
  }

  private startPollingSelf(): void {
    clearTimeout(this._selfTimeout);

    this._selfTimer = setTimeout(() => {
      this.getUserInfo()
        .then()
        .finally(() => {
          this.startPollingSelf();
        });
    }, this._selfTimeout);
  }

  private on401: any = () => {
    if (this.polling) {
      return;
    }

    const renew = this.storage.persistent.get('renew');
    if (renew) {
      return;
    }

    // Block rest api
    this.ippApi.block();

    // Remove access token
    this.ippApi.tokens.access_token = '';
    this.storage.persistent.remove('access_token');
    this.storage.persistent.create('renew', '1');

    this.emit(this.EVENT_RE_LOGGING); // @todo do we need this?

    // Try to authenticate
    this.authenticate(true)
      .then(() => {
        // @todo oh no....
        this.storage.user.suffix = this._user.id;

        this.emit(this.EVENT_LOGIN_REFRESHED);
      })
      .catch(() => {
        this.emit(this.EVENT_ERROR);
      })
      .finally(() => {
        this.storage.persistent.remove('renew');
        this.ippApi.unblock();
      });
  };
}
