import { Observable, interval } from 'rxjs';
import {
  distinctUntilChanged, map, shareReplay, startWith, tap
} from 'rxjs/operators';

import { Anvil } from './anvil';
import { IAuthorizationOptions, Issuer } from './issuer';
import { Scopes } from './scopes';

interface ICommonClaims {
  aud: string;
  exp: number;
  iat: number;
  iss: string;
  sub: string;
}

export interface IAccessClaims extends ICommonClaims {
  jti: string;
  scope: string;
}

export interface IIdClaims extends ICommonClaims {
  at_hash: string;
  nonce: string;
}

export interface ISessionData {
  access_claims: IAccessClaims;
  access_token: string;

  id_claims: IIdClaims;
  id_token: string;

  expires_in: string;
  token_type: string;

  options?: IAuthorizationOptions;
  session_state?: string;
  state?: string;
  user_info?: Object;
}

export class Session {
  public access_token: string;
  public expiration_date: Date;
  public expired$: Observable<boolean>;
  public scopes: Scopes;
  public subject: string;

  constructor(private issuer: Issuer, private session_data?: ISessionData) {
    if(session_data) {
      this.setTokenData(
        session_data.access_token,
        session_data.id_claims.exp,
        session_data.access_claims.scope,
        session_data.access_claims.sub
      );
    } else {
      this.setTokenData('', 0, '', '');
    }

    this.expired$ = interval(0.5).pipe(
      map(() => this.expired),
      distinctUntilChanged(),
      startWith(this.expired),
      // This is only acceptable as long as the `distinctUntilChanged` call is
      // in this chain! If it is removed `abort` will keep being called every
      // emission of the `Observable` once the `Session` expires
      tap(expired => {
        if(expired) {
          this.abort();
        }
      }),
      shareReplay(1)
    );
  }

  abort() {
    if(this.session_data === Anvil.session) {
      if(this.session_data.options.force_path) {
        // As the regular reset method only removes the cookie containing the
        // secret for the current path, the one for the forced path has to be
        // removed manually
        let expires = new Date(0).toUTCString();
        let path = decodeURIComponent(this.session_data.options.force_path);
        document.cookie = `anvil.connect=; expires=${expires}; path=${path}`;
      }

      Anvil.reset();
    }

    // Force the session to be 'expired' as it's no longer valid when aborted
    this.setTokenData('', 0, '', '');

    this.issuer.removeSession(this);
  }

  get expired(): boolean {
    return this.expiration_date <= new Date();
  }

  get persisted(): boolean {
    return this.issuer.persisted_session === this;
  }

  get user_info() {
    if(this.session_data) {
      return this.session_data.user_info;
    } else {
      return null;
    }
  }

  private setTokenData(
    access_token: string,
    expiration_date_s: number,
    scopes: string,
    subject: string
  ) {
    this.access_token = access_token;
    // Token expirations are in seconds, JavaScript operates using milliseconds
    this.expiration_date = new Date(expiration_date_s * 1000);
    this.scopes = new Scopes(scopes);
    this.subject = subject;
  }
}
