
import { Base, List, makeList, CalApiParams, AxiosError, parseAxiosErrorMessage } from '@or-sdk/base';
import { SdkApi } from '@or-sdk/sdk-api';

import { SERVICE_KEY } from './constants';
import { CreateUserConflictError } from './errors';
import {
  currentMultiUserProfileSchema,
  createUserWithMultDataSchema,
  upsertMultiUserParamsSchema,
  usersConfigSchema,
  listUsersParamsSchema,
  getUserByIdParamsSchema,
  listProfilesParamsSchema,
  listUsersAdvancedParamsSchema,
} from './schemas/index';
import type {
  AccountProfile,
  AccountItem,
  CreateUserWithMultData,
  CreateUserWithMultResult,
  CurrentMultiUserProfile,
  GetUserByIdParams,
  ListAccountProfilesResult,
  ListProfilesParams,
  ListProfilesResult,
  ListUsersParams,
  ListUsersResult,
  ListUsersAdvancedParams,
  UserAdvanced,
  ProfileItem,
  UpsertMultiUserParams,
  UpsertMultiUserResult,
  User,
  UsersConfig,
} from './schemas/index';
import type {
  AttachParams,
  AttachResult,
  Profile, ProfileAccountItem, ProfileUserItem, TokenData,
  ListAccountsParams, ListAccountsResult,
  ValidateParams,
  UpdateUserParam,
} from './types';

export class Users extends Base {
  private readonly sdkApi!: SdkApi;

  constructor(params: UsersConfig) {
    const { token, discoveryUrl = '', accountId, usersUrl, sdkUrl } = usersConfigSchema.parse(params);

    super({
      token,
      discoveryUrl,
      serviceKey: SERVICE_KEY,
      accountId,
      serviceUrl: usersUrl,
    });

    if (sdkUrl || discoveryUrl) {
      this.sdkApi = new SdkApi({
        token,
        discoveryUrl,
        accountId,
        sdkUrl,
      });
    }
  }

  public makeRequest<T>(params: CalApiParams): Promise<T> {
    return this.callApiV2(params);
  }

  /**
   * List users in account
   *
   * ```typescript
   * await users.listUsers({
   *   from: 0,
   *   size: 1,
   *   orderDirection: 'asc',
   *   orderProperty: 'email',
   *   query: {
   *     // optional
   *   },
   * });
   * ```
   */
  public async listUsers(params = {} as ListUsersParams): Promise<ListUsersResult> {
    const { from = 0, size, orderDirection, orderProperty, query } = listUsersParamsSchema.parse(params);
    return this.callApiV2({
      route: '/users',
      method: 'GET',
      params: {
        from,
        size,
        orderDirection,
        orderProperty,
        query,
      },
    });
  }

  /**
   * Get account user by Id
   *
   * ```typescript
   * const result = await users.getUserById('<user-id>');
   * ```
   */
  public async getUserById(
    userId: GetUserByIdParams['userId'],
    projection: GetUserByIdParams['projection'],
  ): Promise<User> {
    const { userId: parsedUserId, projection: parsedProjection = [] } = getUserByIdParamsSchema.parse({
      userId,
      projection,
    });
    const accountPrefix = this.sdkApi.isCrossAccount ? `accounts/${this.sdkApi.currentAccountId}` : '';
    const route = `${accountPrefix}/users/${encodeURIComponent(parsedUserId)}`;

    return this.sdkApi.makeRequest({
      route,
      method: 'GET',
      params: { projection: parsedProjection },
    });
  }

  /**
   * Get multi-user profile using user token
   *
   * Will throw error if response from server does not comply with schema
   * ```typescript
   * const profile = await instance.getCurrentMultiUserProfile();
   * ```
   *
   *  To prevent validation of response use:
   * ```typescript
   * const profile = await instance.getCurrentMultiUserProfile({ validate: false });
   * ```
   */
  public async getCurrentMultiUserProfile(
    { validate }: ValidateParams = { validate: true },
  ): Promise<CurrentMultiUserProfile> {
    const profile = await this.sdkApi.makeRequest<CurrentMultiUserProfile>({
      method: 'GET',
      route: '/user/multi-user',
    });
    return validate
      ? await currentMultiUserProfileSchema.parseAsync(profile)
      : profile;
  }

  /**
   * Get account info based on given token
   *
   * ```typescript
   * await users.getAccountInfo();
   * ```
   */
  public async getAccountInfo(): Promise<AccountItem> {
    return this.callApiV2({
      route: '/account',
      method: 'GET',
    });
  }

  /**
   * List account multi-user profiles
   *
   * ```typescript
   * await users.listAccountProfiles();
   * ```
   */
  public async listAccountProfiles(): Promise<ListAccountProfilesResult> {
    return this.callApiV2({
      route: '/account/multi-users',
      method: 'GET',
    });
  }

  /**
   * List profiles with ability for partial match by email, username or match by id.
   *
   * Super admin only.
   *
   * ```typescript
   * await users.listProfiles();
   * ```
   */
  public async listProfiles(params: ListProfilesParams): Promise<ListProfilesResult> {
    const parsedParams = listProfilesParamsSchema.parse(params);
    const data = await this.callApiV2<ProfileItem[]>({
      route: '/multi-users',
      method: 'GET',
      params: parsedParams,
    });
    return makeList<ProfileItem>(data);
  }

  /**
   * List users with profile information
   *
   * ```typescript
   * const result = await users.listUsersAdvanced();
   * ```
   */
  public async listUsersAdvanced(params: ListUsersAdvancedParams): Promise<UserAdvanced[]> {
    const parsedParams = listUsersAdvancedParamsSchema.parse(params);
    return this.callApiV2({
      route: '/users/advanced',
      method: 'GET',
      params: {
        ...parsedParams,
        skipShareViews: parsedParams?.skipShareViews || undefined,
      },
    });
  }

  /**
   * Get profile by email
   *
   * Super admin only.
   *
   * ```typescript
   * const result = await users.getProfileByEmail('multi-user@example.com');
   * ```
   */
  public async getProfileByEmail(email: string): Promise<AccountProfile> {
    return this.sdkApi.makeRequest<AccountProfile>({
      route: `/multi-user/${encodeURIComponent(email)}`,
      method: 'GET',
    });
  }

  /**
   * Get multi-user profile by Id
   * ```typescript
   * const result = await users.getProfileById('<profile-id>');
   * ```
   */
  public async getProfileById(profileId: string): Promise<Profile> {
    return this.sdkApi.makeRequest({
      route: `/multi-user/profile/${encodeURIComponent(profileId)}`,
      method: 'GET',
    });
  }

  /**
   * Get profile
   * ```typescript
   * const result = await users.getProfile();
   * ```
   */
  public async getProfile(): Promise<Profile> {
    return this.callApiV2({
      route: '/users/profile',
      method: 'GET',
    });
  }

  /**
   * Attach profile.
   *
   * ### Normal: Attach current user (based on token) to multi-user profile
   *
   * Pass only multi-user email and it will begin attachment process with email verification.
   * ```typescript
   * await users.attachProfile({
   *   email: 'multi-user@example.com',
   * })
   * ```
   *
   * ### Super admin: Attach another user to multi-user profile
   * Send userId if you want to attach another user instead of current.
   *
   * ```typescript
   * await users.attachProfile({
   *   email: 'multi-user@example.com',
   *   userId: '<user-id-uuid>',
   * });
   * ```
   *
   * ### Super admin: Attach another user to multi-user profile and create new profile if it doesn't exist
   * Send password if you want to automatically create profile.
   *
   * ```typescript
   * await users.attachProfile({
   *   email: 'multi-user@example.com',
   *   userId: '<user-id-uuid>',
   *   password: 's3cure_p@ss0rd!',
   * });
   * ```
   */
  public async attachProfile(params: AttachParams): Promise<AttachResult> {
    return this.sdkApi.makeRequest({
      route: `/accounts/${this.currentAccountId ?? 'current'}/multi-user/attach-start`,
      method: 'POST',
      data: {
        email: params.email,
        userId: params.userId,
        password: params.password,
      },
    });
  }

  /**
   * List profiles users.
   * ```typescript
   * await users.listProfileUsers();
   * ```
   */
  public async listProfileUsers(): Promise<List<ProfileUserItem>> {
    return this.sdkApi.makeRequest({
      route: '/multi-user/list-users',
      method: 'GET',
    });
  }

  /**
   * Get current user
   * ```typescript
   * const result = await users.getCurrentUser();
   * ```
   */
  public async getCurrentUser(): Promise<TokenData> {
    return this.sdkApi.makeRequest<TokenData>({
      method: 'GET',
      route: '/auth/token',
    });
  }

  /**
   * Get profile email if exists or user email
   * ```typescript
   * const result = await users.getUserEmail();
   * ```
   */
  public async getUserEmail(userId?: string): Promise<{ email: string; }> {
    return this.callApiV2<{ email: string; }>({
      route: `/users/${userId ?? 'current'}/email`,
      method: 'GET',
    });
  }

  /**
   * Delete a user from account
   * ```typescript
   * await users.deleteUser(userId, accountId);
   * ```
   */
  public async deleteUser(userId: string, accountId?: string): Promise<void> {
    const route = `${accountId ? `/accounts/${accountId}` : ''}/users/${userId}`;

    return this.sdkApi.makeRequest<void>({
      route,
      method: 'DELETE',
    });
  }

  /**
   * Update user
   * ```typescript
   * await users.updateUser(userId, user, accountId);
   * ```
   */
  public async updateUser(userId: string, user: UpdateUserParam, accountId?: string): Promise<void> {
    const route = `${accountId ? `/accounts/${accountId}` : ''}/users/${userId}`;

    return this.sdkApi.makeRequest<void>({
      route: route,
      method: 'PUT',
      data: {
        data: user.data,
        email: user.email,
        username: user.username,
        role: user.role,
        multiUserId: user.multiUserId,
      },
    });
  }

  /**
   * Request force change password for user
   * ```typescript
   * await users.requestForceChangePassword(userId, multi);
   * ```
   */
  public async requestForceChangePassword(userId: string, multi?: boolean): Promise<void> {
    return await this.sdkApi.makeRequest<void>({
      route: '/multi-user/force-change-password',
      method: 'PUT',
      data: {
        userId,
        multi,
      },
    });
  }

  /**
   * Disable user
   * ```typescript
   * await users.disableUser(userId, accountId);
   * ```
   */
  public async disableUser(userId: string, accountId?: string): Promise<void> {
    const route = `${accountId ? `/accounts/${accountId}` : ''}/users/${userId}/disable`;

    return this.sdkApi.makeRequest<void>({
      route,
      method: 'PUT',
    });
  }

  /**
   * Enable user
   * ```typescript
   * await users.enableUser(userId, accountId);
   * ```
   */
  public async enableUser(userId: string, accountId?: string): Promise<void> {
    const route = `${accountId ? `/accounts/${accountId}` : ''}/users/${userId}/enable`;

    return this.sdkApi.makeRequest<void>({
      route,
      method: 'PUT',
    });
  }

  /**
   * Initiate the creation of the multi-user
   *
   * @example
   * ```typescript
   * const { userCreated, emailSent } = await users.createUserWithMulti(userData, accountId);
   * // userCreated === true - if user already exists
   * // emailSent === true - if user does not exists and invitation email was sent
   * ```
   */
  public async createUserWithMulti(
    userData: CreateUserWithMultData,
    accountId = this.targetAccountId,
  ): Promise<CreateUserWithMultResult> {
    try {
      const data = await createUserWithMultDataSchema.parseAsync(userData);

      return await this.sdkApi.makeRequest<CreateUserWithMultResult>({
        route: `/accounts/${accountId ?? undefined}/multi-user/create-start`,
        method: 'POST',
        data,
      });
    } catch (error) {
      if (error instanceof Error && error.cause instanceof AxiosError && error.cause?.response?.status === 409) {
        const errorMessage = parseAxiosErrorMessage(error.cause);
        throw new CreateUserConflictError(errorMessage, {
          cause: error.cause,
          statusCode: error.cause.response.status,
        });
      }
      if (error instanceof AxiosError && error.response?.status === 409) {
        const errorMessage = parseAxiosErrorMessage(error);
        throw new CreateUserConflictError(errorMessage, {
          cause: error,
          statusCode: error.response.status,
        });
      }
      throw error;
    }
  }

  /**
   * List accounts by profile
   * ```typescript
   * await users.userListProfileAccounts();
   * ```
   */
  public async userListProfileAccounts(): Promise<ProfileAccountItem[]> {
    return this.callApiV2({
      route: '/multi-users/accounts',
    });
  }

  /**
   * List accounts (super admin only)
   * ```typescript
   * await users.listAccounts({limit, skip, query, projection});
   * ```
   */
  public async listAccounts(params: ListAccountsParams = {}): Promise<ListAccountsResult> {
    const data = await this.callApiV2<{ count: number; rows: ListAccountsResult['items']; }>({
      route: '/accounts',
      params,
    });
    return {
      total: data.count,
      items: data.rows,
    };
  }

  /**
   * Upsert multi-user.
   *
   * If multi-user with email already exists return it, otherwise create new one with given email and password
   *
   * Super admin only!
   *
   * @example
   * ```typescript
   * await users.createMultiUser({
   *   email: 'user@example.com',
   *   password: 's3cure_p@ss0rd!',
   *   data: {
   *     // optional
   *   }
   * });
   * ```
   */
  public async upsertMultiUser(params: UpsertMultiUserParams): Promise<UpsertMultiUserResult> {
    const data = await upsertMultiUserParamsSchema.parseAsync(params);

    return await this.sdkApi.makeRequest<UpsertMultiUserResult>({
      route: '/multi-user/create',
      method: 'PUT',
      data,
    });
  }
}
