import {Provider} from '@loopback/context';
import axios from 'axios';
import qs from 'qs';
import {ConfigureIdpFunc, IdpDetails, IdpResp} from '../../types';
import AWS from 'aws-sdk';
import {randomBytes} from 'crypto';

export const STATUS = {
  OK: 200,
  NOT_FOUND: 404,
};

interface TokenResponse {
  // eslint-disable-next-line
  access_token: string;
}

export class KeycloakIdpProvider
  implements Provider<ConfigureIdpFunc<IdpResp>>
{
  ssm: AWS.SSM;

  constructor() {
    this.ssm = new AWS.SSM({region: process.env.AWS_REGION});
  }

  value(): ConfigureIdpFunc<IdpResp> {
    return payload => this.configure(payload);
  }

  async configure(payload: IdpDetails): Promise<IdpResp> {
    const {tenant, plan} = payload;

    try {
      const token = await this.authenticateAdmin();

      // Fetch the clientId, clientSecret, and realmName from AWS SSM
      const clientId = await this.getParameterFromSSM(
        `/${process.env.NAMESPACE}/${process.env.ENVIRONMENT}/${plan.tier.toLowerCase()}/${tenant.key}/keycloak-client-id`,
      );
      const clientSecret = await this.getParameterFromSSM(
        `/${process.env.NAMESPACE}/${process.env.ENVIRONMENT}/${plan.tier.toLowerCase()}/${tenant.key}/keycloak-client-secret`,
      );
      const realmName = await this.getParameterFromSSM(
        `/${process.env.NAMESPACE}/${process.env.ENVIRONMENT}/${plan.tier.toLowerCase()}/${tenant.key}/keycloak-client-realm`,
      );

      // Handling the logic based on tenant tier
      if (plan.tier === 'PREMIUM') {
        // For PREMIUM: always create a new realm
        await this.createRealm(realmName ?? tenant.key, token);
      } else if (plan.tier === 'STANDARD' || plan.tier === 'BASIC') {
        // For STANDARD or BASIC: check if the realm exists
        const realmExists = await this.realmExists(
          realmName ?? tenant.key,
          token,
        );
        if (!realmExists) {
          // If the realm does not exist, create it
          await this.createRealm(realmName ?? tenant.key, token);
        }
      } else {
        // DO NOTHING
      }

      // Set up SMTP settings in the realm for AWS SES
      await this.setupEmailSettings(realmName ?? tenant.key, token);

      // Create a new client within the realm
      await this.createClient(
        realmName ?? tenant.key,
        clientId,
        token,
        clientSecret,
        tenant.key,
      );

      // Create a new admin user for the tenant
      const adminUsername = tenant.contacts[0].email;
      const passwordLength = 20;
      const adminPassword = this.generateStrongPassword(passwordLength);
      const {firstName, lastName, email} = tenant.contacts[0];

      const user = await this.createUser(
        realmName ?? tenant.key,
        adminUsername,
        adminPassword,
        firstName,
        lastName,
        email,
        token,
      );

      return {
        authId: user.id,
      };
    } catch (error) {
      throw new Error(
        `Failed to configure Keycloak for tenant: ${tenant.name}`,
      );
    }
  }

  // Method to check if a realm exists
  async realmExists(realmName: string, token: string): Promise<boolean> {
    try {
      const response = await axios.get(
        `${process.env.KEYCLOAK_HOST}/admin/realms/${realmName}`,
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      );
      // If the realm exists, a successful response is returned (status code 200)
      return response.status === STATUS.OK;
    } catch (error) {
      if (error.response && error.response.status === STATUS.NOT_FOUND) {
        // If a 404 is returned, it means the realm doesn't exist
        return false;
      }
      // Rethrow any other errors
      throw new Error(`Error checking realm existence: ${error.message}`);
    }
  }

  // Method to authenticate as Keycloak Admin
  async authenticateAdmin(): Promise<string> {
    const response = await axios.post<TokenResponse>(
      `${process.env.KEYCLOAK_HOST}/realms/master/protocol/openid-connect/token`,
      qs.stringify({
        username: process.env.KEYCLOAK_ADMIN_USERNAME,
        password: process.env.KEYCLOAK_ADMIN_PASSWORD,
        // eslint-disable-next-line
        grant_type: 'password',
        // eslint-disable-next-line
        client_id: 'admin-cli',
      }),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
        },
      },
    );

    return response.data.access_token;
  }

  async createRealm(realmName: string, token: string): Promise<void> {
    try {
      await axios.post(
        `${process.env.KEYCLOAK_HOST}/admin/realms`,
        {
          realm: realmName,
          enabled: true,
        },
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      );
    } catch (error) {
      throw new Error(
        `Failed to create realm '${realmName}': ${error.message}`,
      );
    }
  }

  // Method to set up AWS SES SMTP settings in the realm
  async setupEmailSettings(realmName: string, token: string): Promise<void> {
    try {
      await axios.put(
        `${process.env.KEYCLOAK_HOST}/admin/realms/${realmName}`,
        {
          smtpServer: {
            auth: true,
            starttls: true, // Enables TLS
            host: process.env.AWS_SES_SMTP_HOST, // Example: email-smtp.us-east-1.amazonaws.com
            port: '587', // Use port 587 for TLS
            user: process.env.AWS_SES_SMTP_USERNAME, // Your AWS SES SMTP username
            password: process.env.AWS_SES_SMTP_PASSWORD, // Your AWS SES SMTP password
            from: process.env.SMTP_FROM_EMAIL, // The "from" email address, e.g. 'no-reply@yourdomain.com'
            fromDisplayName: process.env.SMTP_FROM_DISPLAY_NAME, // The display name, e.g. 'Your Company Name'
          },
        },
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      );
    } catch (error) {
      throw new Error(
        `Failed to set up email settings for realm '${realmName}': ${error.message}`,
      );
    }
  }
  // Method to create a new Keycloak client
  async createClient(
    realmName: string,
    clientId: string,
    token: string,
    clientSecret: string,
    key: string,
  ): Promise<void> {
    try {
      const redirectUris = [
        'http://localhost:3000/*',
        `https://${key}.${process.env.DOMAIN_NAME}/authentication-service/*`,
      ];

      await axios.post(
        `${process.env.KEYCLOAK_HOST}/admin/realms/${realmName}/clients`,
        {
          clientId: clientId,
          publicClient: false, // Must be false for client authentication
          secret: clientSecret,
          directAccessGrantsEnabled: true,
          protocol: 'openid-connect',
          enabled: true,
          redirectUris: redirectUris,
          clientAuthenticatorType: 'client-secret', // Enable client authentication
        },
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      );
    } catch (error) {
      throw new Error(
        `Failed to create client '${clientId}' in realm '${realmName}': ${error.message}`,
      );
    }
  }

  // Method to create a new Keycloak user
  async createUser(
    realmName: string,
    username: string,
    password: string,
    firstName: string,
    lastName: string,
    email: string,
    token: string,
  ): Promise<{id: string}> {
    try {
      const createUserResponse = await axios.post(
        `${process.env.KEYCLOAK_HOST}/admin/realms/${realmName}/users`,
        {
          username: username,
          enabled: true,
          firstName: firstName,
          lastName: lastName,
          email: email,
          emailVerified: true,
          credentials: [
            {
              type: 'password',
              value: password,
              temporary: true, // Set password as temporary
            },
          ],
        },
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      );

      const locationHeader = createUserResponse.headers['location'];
      if (!locationHeader) {
        throw new Error(
          "User creation failed, no 'Location' header in response.",
        );
      }

      const userId = locationHeader.split('/').pop();
      if (!userId) {
        throw new Error(
          "User creation failed, could not extract user ID from 'Location' header.",
        );
      }

      // Send the password reset email
      await this.sendPasswordResetEmail(realmName, userId, token);

      return {id: userId};
    } catch (error) {
      throw new Error(
        `Failed to create user '${username}' in realm '${realmName}': ${error.message}`,
      );
    }
  }

  // Method to send a password reset email
  async sendPasswordResetEmail(
    realmName: string,
    userId: string,
    token: string,
  ): Promise<void> {
    try {
      await axios.put(
        `${process.env.KEYCLOAK_HOST}/admin/realms/${realmName}/users/${userId}/execute-actions-email`,
        ['UPDATE_PASSWORD'],
        {
          headers: {
            Authorization: `Bearer ${token}`,
          },
        },
      );
    } catch (error) {
      throw new Error(
        `Failed to send password reset email for user '${userId}' in realm '${realmName}': ${error.message}`,
      );
    }
  }

  // Helper function to fetch parameters from AWS SSM with error handling
  async getParameterFromSSM(parameterName: string): Promise<string> {
    try {
      const response = await this.ssm
        .getParameter({Name: parameterName, WithDecryption: true})
        .promise();
      return response.Parameter?.Value ?? '';
    } catch (error) {
      // Optionally, you can throw the error or return a default value
      throw new Error(`Failed to fetch parameter ${parameterName}`);
    }
  }

  generateStrongPassword(length: number): string {
    const regex = /[A-Za-z0-9!@#$%^&*()_+~`|}{[\]:;?><,./-=]/; //NOSONAR
    const validChars: string[] = [];

    const ASCII_PRINTABLE_START = 33;

    const ASCII_PRINTABLE_END = 126;

    for (let i = ASCII_PRINTABLE_START; i <= ASCII_PRINTABLE_END; i++) {
      const char = String.fromCharCode(i);
      if (regex.test(char)) {
        validChars.push(char);
      }
    }
    const randomBytesArray = randomBytes(length);
    const password = Array.from(randomBytesArray)
      .map(byte => validChars[byte % validChars.length])
      .join('');
    return password;
  }
}
