/*
 * © 2021 Thoughtworks, Inc.
 */

import { AthenaClient } from '@aws-sdk/client-athena'
import { CloudWatchClient } from '@aws-sdk/client-cloudwatch'
import { CloudWatchLogsClient } from '@aws-sdk/client-cloudwatch-logs'
import { CostExplorerClient } from '@aws-sdk/client-cost-explorer'
import { GlueClient } from '@aws-sdk/client-glue'
import { S3Client } from '@aws-sdk/client-s3'
import type { AwsCredentialIdentity, Provider } from '@aws-sdk/types'

type AwsClientConfig = {
  region: string
  credentials: AwsCredentialIdentity | Provider<AwsCredentialIdentity>
}

import {
  CloudProviderAccount,
  ComputeEstimator,
  EmbodiedEmissionsEstimator,
  ICloudService,
  MemoryEstimator,
  NetworkingEstimator,
  Region,
  StorageEstimator,
  UnknownEstimator,
} from '@cloud-carbon-footprint/core'
import {
  AWS_RECOMMENDATIONS_TARGETS,
  configLoader,
  EstimationResult,
  GroupBy,
  LookupTableInput,
  LookupTableOutput,
  RecommendationResult,
} from '@cloud-carbon-footprint/common'

import {
  CostAndUsageReports,
  EBS,
  EC2,
  ElastiCache,
  Lambda,
  RDS,
  RDSComputeService,
  RDSStorage,
  S3,
  ServiceWrapper,
} from '../lib'

import AWSCredentialsProvider from './AWSCredentialsProvider'

import {
  AWS_CLOUD_CONSTANTS,
  AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
} from '../domain'
import { Recommendations } from '../lib/Recommendations'

export default class AWSAccount extends CloudProviderAccount {
  private readonly credentials: Provider<AwsCredentialIdentity>

  constructor(
    public id: string,
    public name: string,
    private regions: string[],
  ) {
    super()
    this.credentials = AWSCredentialsProvider.create(id)
  }

  async getDataForRegions(
    startDate: Date,
    endDate: Date,
    grouping: GroupBy,
  ): Promise<EstimationResult[]> {
    const results: EstimationResult[][] = []
    for (const regionId of this.regions) {
      const regionEstimates: EstimationResult[] = await Promise.all(
        await this.getDataForRegion(regionId, startDate, endDate, grouping),
      )
      results.push(regionEstimates)
    }

    return results.flat()
  }

  getDataForRegion(
    regionId: string,
    startDate: Date,
    endDate: Date,
    grouping: GroupBy,
  ): Promise<EstimationResult[]> {
    const awsServices = this.getServices(regionId)
    const awsConstants = {
      minWatts: AWS_CLOUD_CONSTANTS.MIN_WATTS_AVG,
      maxWatts: AWS_CLOUD_CONSTANTS.MAX_WATTS_AVG,
      powerUsageEffectiveness: AWS_CLOUD_CONSTANTS.getPUE(),
    }
    const region = new Region(
      regionId,
      awsServices,
      AWS_EMISSIONS_FACTORS_METRIC_TON_PER_KWH,
      awsConstants,
    )
    return this.getRegionData(
      configLoader().AWS.NAME,
      region,
      startDate,
      endDate,
      grouping,
    )
  }

  getServices(regionId: string): ICloudService[] {
    return configLoader().AWS.CURRENT_SERVICES.map(({ key }) => {
      return this.getService(key, regionId, this.credentials)
    })
  }

  async getDataForRecommendations(
    recommendationTarget: AWS_RECOMMENDATIONS_TARGETS,
  ): Promise<RecommendationResult[]> {
    const serviceWrapper = this.createServiceWrapper(
      this.getClientConfig(configLoader().AWS.ATHENA_REGION, this.credentials),
    )

    return await Recommendations.getRecommendations(
      recommendationTarget,
      serviceWrapper,
    )
  }

  async getDataFromCostAndUsageReports(
    startDate: Date,
    endDate: Date,
    grouping: GroupBy,
  ): Promise<EstimationResult[]> {
    const costAndUsageReportsService = new CostAndUsageReports(
      new ComputeEstimator(),
      new StorageEstimator(AWS_CLOUD_CONSTANTS.SSDCOEFFICIENT),
      new StorageEstimator(AWS_CLOUD_CONSTANTS.HDDCOEFFICIENT),
      new NetworkingEstimator(AWS_CLOUD_CONSTANTS.NETWORKING_COEFFICIENT),
      new MemoryEstimator(AWS_CLOUD_CONSTANTS.MEMORY_COEFFICIENT),
      new UnknownEstimator(AWS_CLOUD_CONSTANTS.ESTIMATE_UNKNOWN_USAGE_BY),
      new EmbodiedEmissionsEstimator(
        AWS_CLOUD_CONSTANTS.SERVER_EXPECTED_LIFESPAN,
      ),
      this.createServiceWrapper(
        this.getClientConfig(
          configLoader().AWS.ATHENA_REGION,
          this.credentials,
        ),
      ),
    )
    return await costAndUsageReportsService.getEstimates(
      startDate,
      endDate,
      grouping,
    )
  }

  static async getCostAndUsageReportsDataFromInputData(
    inputData: LookupTableInput[],
  ): Promise<LookupTableOutput[]> {
    const costAndUsageReportsService = new CostAndUsageReports(
      new ComputeEstimator(),
      new StorageEstimator(AWS_CLOUD_CONSTANTS.SSDCOEFFICIENT),
      new StorageEstimator(AWS_CLOUD_CONSTANTS.HDDCOEFFICIENT),
      new NetworkingEstimator(AWS_CLOUD_CONSTANTS.NETWORKING_COEFFICIENT),
      new MemoryEstimator(AWS_CLOUD_CONSTANTS.MEMORY_COEFFICIENT),
      new UnknownEstimator(AWS_CLOUD_CONSTANTS.ESTIMATE_UNKNOWN_USAGE_BY),
      new EmbodiedEmissionsEstimator(
        AWS_CLOUD_CONSTANTS.SERVER_EXPECTED_LIFESPAN,
      ),
    )
    return await costAndUsageReportsService.getEstimatesFromInputData(inputData)
  }

  private getService(
    key: string,
    region: string,
    credentials: AwsClientConfig['credentials'],
  ): ICloudService {
    if (this.services[key] === undefined)
      throw new Error('Unsupported service: ' + key)
    const options = this.getClientConfig(region, credentials)
    return this.services[key](options)
  }

  private getClientConfig(
    region: string,
    credentials: AwsClientConfig['credentials'],
  ): AwsClientConfig {
    return { region, credentials }
  }

  private createServiceWrapper(options: AwsClientConfig) {
    return new ServiceWrapper(
      new CloudWatchClient(options),
      new CloudWatchLogsClient(options),
      new CostExplorerClient({
        region: configLoader().AWS.IS_AWS_GLOBAL
          ? 'us-east-1'
          : 'cn-northwest-1',
        credentials: options.credentials,
      }),
      new S3Client(options),
      new AthenaClient(options),
      new GlueClient(options),
    )
  }

  private services: {
    [id: string]: (options: AwsClientConfig) => ICloudService
  } = {
    ebs: (options) => {
      return new EBS(this.createServiceWrapper(options))
    },
    s3: (options) => {
      return new S3(this.createServiceWrapper(options))
    },
    ec2: (options) => {
      return new EC2(this.createServiceWrapper(options))
    },
    elasticache: (options) => {
      return new ElastiCache(this.createServiceWrapper(options))
    },
    rds: (options) => {
      return new RDS(
        new RDSComputeService(this.createServiceWrapper(options)),
        new RDSStorage(this.createServiceWrapper(options)),
      )
    },
    lambda: (options) => {
      return new Lambda(120000, 1000, this.createServiceWrapper(options))
    },
  }
}
