import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants";
import * as Logger from "../Common/Logger";
import { configContext } from "../ConfigContext";
import { AuthorizeAccessComponent } from "../Explorer/Controls/GitHub/AuthorizeAccessComponent";
import { ConsoleDataType } from "../Explorer/Menus/NotificationConsole/NotificationConsoleComponent";
import { JunoClient } from "../Juno/JunoClient";
import { isInvalidParentFrameOrigin } from "../Utils/MessageValidation";
import * as NotificationConsoleUtils from "../Utils/NotificationConsoleUtils";
import { GitHubConnectorMsgType, IGitHubConnectorParams } from "./GitHubConnector";

window.addEventListener("message", (event: MessageEvent) => {
  if (isInvalidParentFrameOrigin(event)) {
    return;
  }

  const msg = event.data;
  if (msg.type === GitHubConnectorMsgType) {
    const params = msg.data as IGitHubConnectorParams;
    window.dataExplorer.notebookManager?.gitHubOAuthService.finishOAuth(params);
  }
});

export interface IGitHubOAuthToken {
  // API properties
  access_token?: string;
  scope?: string;
  token_type?: string;
  error?: string;
  error_description?: string;
}

export class GitHubOAuthService {
  private static readonly OAuthEndpoint = "https://github.com/login/oauth/authorize";

  private state: string;
  private token: ko.Observable<IGitHubOAuthToken>;

  constructor(private junoClient: JunoClient) {
    this.token = ko.observable<IGitHubOAuthToken>();
  }

  public async startOAuth(scope: string): Promise<string> {
    // If attempting to change scope from "Public & private repos" to "Public only" we need to delete app authorization.
    // Otherwise OAuth app still retains the "public & private repos" permissions.
    if (
      this.token()?.scope === AuthorizeAccessComponent.Scopes.PublicAndPrivate.key &&
      scope === AuthorizeAccessComponent.Scopes.Public.key
    ) {
      const logoutSuccessful = await this.logout();
      if (!logoutSuccessful) {
        return undefined;
      }
    }

    const params = {
      scope,
      client_id: configContext.GITHUB_CLIENT_ID,
      redirect_uri: new URL("./connectToGitHub.html", window.location.href).href,
      state: this.resetState()
    };

    window.open(`${GitHubOAuthService.OAuthEndpoint}?${new URLSearchParams(params).toString()}`);
    return params.state;
  }

  public async finishOAuth(params: IGitHubConnectorParams) {
    try {
      this.validateState(params.state);
      const response = await this.junoClient.getGitHubToken(params.code);

      if (response.status === HttpStatusCodes.OK && !response.data.error) {
        NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, "Successfully connected to GitHub");
        this.token(response.data);
      } else {
        let errorMsg = response.data.error;
        if (response.data.error_description) {
          errorMsg = `${errorMsg}: ${response.data.error_description}`;
        }
        throw new Error(errorMsg);
      }
    } catch (error) {
      NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, `Failed to connect to GitHub: ${error}`);
      this.token({ error });
    }
  }

  public getTokenObservable(): ko.Observable<IGitHubOAuthToken> {
    return this.token;
  }

  public async logout(): Promise<boolean> {
    try {
      const response = await this.junoClient.deleteAppAuthorization(this.token()?.access_token);
      if (response.status !== HttpStatusCodes.NoContent) {
        throw new Error(`Received HTTP ${response.status}: ${response.data} when deleting app authorization`);
      }

      this.resetToken();
      return true;
    } catch (error) {
      const message = `Failed to delete app authorization: ${error}`;
      Logger.logError(message, "GitHubOAuthService/logout");
      NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
      return false;
    }
  }

  public isLoggedIn(): boolean {
    return !!this.token()?.access_token;
  }

  private resetState(): string {
    this.state = Math.floor(Math.random() * Math.floor(Number.MAX_SAFE_INTEGER)).toString();
    return this.state;
  }

  public resetToken() {
    this.token(undefined);
  }

  private validateState(state: string) {
    if (state !== this.state) {
      throw new Error("State didn't match. Possibility of cross-site request forgery attack.");
    }
  }
}