
import { AuthConfiguration } from "./types";
import { config } from "../config";
import { Issuer, Client, errors, DeviceFlowHandle, TokenSet } from "openid-client";
import * as fs from 'fs';
import * as path from 'path';

export class ApiKeyAuth implements AuthConfiguration {
  apiKey: string;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  async getAuthHeader() {
    return `Token ${this.apiKey}`;
  }
}

// Modified from original example here: https://github.com/auth0/auth0-device-flow-cli-sample/blob/master/cli.js
// Device Flow Auth Scheme - https://tools.ietf.org/html/rfc8628
export class DeviceAuth implements AuthConfiguration {
  apiKey: string | undefined;

  auth0Domain: string;
  clientId: string;
  scope: string;
  audience: string;

  auth0Client: Client | undefined;

  tokens: TokenSet | undefined;

  constructor(apiKey?: string) {
    this.apiKey = apiKey;
    this.auth0Domain = config.auth0.domain;
    this.clientId = config.auth0.clientId;
    this.scope = "openid profile email offline_access";
    this.audience = config.auth0.audience;
    this.loadTokensFromConfig();
  }

  private loadTokensFromConfig() {
    const tokensConfig = config.auth0.tokens;
    if (tokensConfig === undefined) {
      console.warn("Warning: No tokens found in config file! You will need to authenticate.");
      return;
    } else if (typeof tokensConfig === 'string') {
      try {
        const loadedTokens = JSON.parse(tokensConfig);
        if (Object.keys(loadedTokens).length > 0) {
          this.tokens = new TokenSet(loadedTokens);
        } else {
          console.warn("Warning: No tokens found in config file! You will need to authenticate.");
        }
      } catch (err: any) {
        console.error(`Failed to parse tokens from config: ${err.message}`);
      }
    } else {
      console.error("Tokens in config file are not a string");
    }
  }


  // check to see if the tokens are usable
  // if not send the user through the device flow
  async getAuthHeader() {
    if (!this.tokens) {
      // send the user through the device flow
      const deviceCodeHandle = await this.getDeviceCode();
      await this.requestDeviceActivation(deviceCodeHandle);
      await this.saveTokens();
    } else if (this.tokens.expired()) {
      // refresh the tokens
      try {
        await this.refreshTokens();
        await this.saveTokens();
      } catch (error: unknown) {
        if (error instanceof Error) {
          if (error.message.includes('No refresh token available')) {
            // send the user through the device flow
            const deviceCodeHandle = await this.getDeviceCode();
            await this.requestDeviceActivation(deviceCodeHandle);
            await this.saveTokens();
          }
        }
      }
    } else {
      // tokens are valid
    }
    console.log(`\n\nUsing tokens: ${JSON.stringify(this.tokens, null, 2)}\n\n`);
    return `Bearer ${this.tokens?.access_token}`;
  }

  async saveTokens() {
    // write the tokens to ~/.protege
    const tokens = this.tokens;
    // load current config
    const configPath = path.join(process.env.HOME ?? '~', '.protege');
    const currentConfig = fs.readFileSync(configPath, 'utf8');
    // update tokens in config AUTH0_TOKENS=...
    let newConfig: string;
    if (currentConfig.includes('AUTH0_TOKENS')) {
      newConfig = currentConfig.replace(/AUTH0_TOKENS=.*$/, `AUTH0_TOKENS=${JSON.stringify(tokens)}`);
    } else {
      // add it to the end
      newConfig = currentConfig + `\nAUTH0_TOKENS=${JSON.stringify(tokens)}`;
    }
     // write config
    console.log(`\n\nWriting config to ${configPath}\n\n${newConfig}\n\n------`)
    fs.writeFileSync(configPath, newConfig);
  }

  async initClient() {
    if (!this.auth0Client) {
      const auth0 = await Issuer.discover(`https://${this.auth0Domain}`);
      this.auth0Client = new auth0.Client({
        client_id: this.clientId,
        token_endpoint_auth_method: "none",
        id_token_signed_response_alg: "RS256"
      });
    }
  }

  async getDeviceCode(): Promise<DeviceFlowHandle> {
    await this.initClient();
    // Device Authorization Request - https://tools.ietf.org/html/rfc8628#section-3.1
    const handle = await this.auth0Client?.deviceAuthorization({
      scope: this.scope,
      audience: this.audience
    });
    if (!handle) {
      throw new Error("Failed to get device code");
    }
    // Device Authorization Response - https://tools.ietf.org/html/rfc8628#section-3.2
    return handle;
  }

  async requestDeviceActivation(deviceCodeHandle: DeviceFlowHandle) {
    // User Interaction - https://tools.ietf.org/html/rfc8628#section-3.3
    console.log(`\n\nNavigate to ${deviceCodeHandle.verification_uri_complete} in your browser!\n\n`)


    // Device Access Token Request - https://tools.ietf.org/html/rfc8628#section-3.4
    // Device Access Token Response - https://tools.ietf.org/html/rfc8628#section-3.5
    let tokens;
    try {
      tokens = await deviceCodeHandle.poll();
    } catch (err: any) {
      switch (err.error) {
        case 'access_denied': // end-user declined the device confirmation prompt, consent, or rules failed
          console.error('\n\nAccess denied by user. Please try again.\n\n');
          break;
        case 'expired_token': // the device_code has expired and the device authorization session has concluded
          console.error('\n\nDevice code expired. Please try again.\n\n');
          break;
        default:
          if (err instanceof errors.OPError) {
            console.error(`\n\nerror = ${err.error}; error_description = ${err.error_description}`);
          } else {
            throw err;
          }
      }
    }
    console.log(`\n\n Retrieved tokens: \n\n${JSON.stringify(tokens, null, 2)}\n\n`);

    if (tokens) {
      console.log(`\n\n Retrieved tokens: \n\n${JSON.stringify(tokens, null, 2)}\n\n`);

      // requests without openid scope will not contain an id_token
      if (tokens.id_token) {
        console.log(`\n\n ID Token Claims: ${JSON.stringify(tokens.claims(), null, 2)}\n\n`);
      }

      // validate the id_token
      try {
        const idToken = await this.auth0Client?.userinfo(tokens);
        console.log(`\n\n ID Token Userinfo: ${JSON.stringify(idToken, null, 2)}\n\n`);
        // save the tokens
        this.tokens = tokens;
      } catch (err: any) {
        console.error(`\n\n Failed to validate ID Token: ${err.message}\n\n`);
        throw err;
      }
    }
  }

  async refreshTokens() {
    if (this.tokens && this.tokens.expired()) {
      await this.initClient();
      try {
        if (!this.tokens.refresh_token) {
          throw new Error("No refresh token available");
        }
        this.tokens = await this.auth0Client?.refresh(this.tokens.refresh_token);
      } catch (err: any) {
        console.error(`\n\n Failed to refresh tokens: ${err.message}\n\n`);
        throw err;
      }
    }
  }
}