import { CONTEXT, Inject, Injectable, Scope } from 'graphql-modules';
import { maskToken } from '@hive/service-common';
import type { TokensApi } from '@hive/tokens';
import { createTRPCProxyClient, httpLink } from '@trpc/client';
import type { Token } from '../../../shared/entities';
import { HiveError } from '../../../shared/errors';
import { atomic } from '../../../shared/helpers';
import type {
  OrganizationAccessScope,
  ProjectAccessScope,
  TargetAccessScope,
} from '../../auth/providers/scopes';
import { Logger } from '../../shared/providers/logger';
import type { TargetSelector } from '../../shared/providers/storage';
import type { TokensConfig } from './tokens';
import { TOKENS_CONFIG } from './tokens';

export interface TokenSelector {
  token: string;
}

interface CreateTokenInput extends TargetSelector {
  name: string;
  scopes: Array<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>;
}

export interface CreateTokenResult extends Token {
  secret: string;
}

@Injectable({
  scope: Scope.Operation,
  global: true,
})
export class TokenStorage {
  private logger: Logger;
  private tokensService;

  constructor(
    logger: Logger,
    @Inject(TOKENS_CONFIG) tokensConfig: TokensConfig,
    @Inject(CONTEXT) context: GraphQLModules.ModuleContext,
  ) {
    this.logger = logger.child({ source: 'TokenStorage' });
    this.tokensService = createTRPCProxyClient<TokensApi>({
      links: [
        httpLink({
          url: `${tokensConfig.endpoint}/trpc`,
          fetch,
          headers: {
            'x-request-id': context.requestId,
          },
        }),
      ],
    });
  }

  async createToken(input: CreateTokenInput) {
    this.logger.debug('Creating new token (input=%o)', input);

    const response = await this.tokensService.createToken.mutate({
      name: input.name,
      target: input.targetId,
      project: input.projectId,
      organization: input.organizationId,
      scopes: input.scopes,
    });

    return response;
  }

  async deleteTokens(input: {
    targetId: string;
    tokenIds: readonly string[];
  }): Promise<readonly string[]> {
    this.logger.debug('Deleting tokens (input=%o)', input);

    const deletedIds: Array<string> = [];

    await Promise.all(
      input.tokenIds.map(token =>
        this.tokensService.deleteToken
          .mutate({
            targetId: input.targetId,
            token,
          })
          .then(didDelete => {
            if (!didDelete) {
              return;
            }
            deletedIds.push(token);
          }),
      ),
    );

    return deletedIds;
  }

  async invalidateTokens(tokens: string[]) {
    this.logger.debug('Invalidating tokens (size=%s)', tokens.length);

    await this.tokensService.invalidateTokens
      .mutate({
        tokens,
      })
      .catch(error => {
        this.logger.error(error);
      });
  }

  async getTokens(selector: TargetSelector) {
    this.logger.debug('Fetching tokens (selector=%o)', selector);

    const response = await this.tokensService.targetTokens.query({
      targetId: selector.targetId,
    });

    return response || [];
  }

  @atomic<TokenSelector>(({ token }) => token)
  async getToken({ token }: TokenSelector) {
    try {
      // Tokens are MD5 hashes, so they are always 32 characters long
      if (token.length !== 32) {
        throw new HiveError(`Incorrect length: received ${token.length}, expected 32`);
      }

      this.logger.debug('Fetching token (token=%s)', maskToken(token));
      const tokenInfo = await this.tokensService.getToken.query({ token });

      if (!tokenInfo) {
        throw new HiveError('Not found');
      }

      return tokenInfo;
    } catch (error: any) {
      const errorText =
        error instanceof Error
          ? error.toString()
          : typeof error === 'string'
            ? error
            : JSON.stringify(error);

      this.logger.error('Failed to fetch token (error=%s)', errorText);
      throw new HiveError('Invalid token provided', {
        originalError: error,
      });
    }
  }
}
