import { EmptyArgumentError, NetworkError, RequestError } from './errors';
import { createGQLSdk, GQLSdk } from './gqlClient';
import { CurrentGroupQuery, FeatureType } from './graphql/generated/gqlTypes';
import {
  KanaPublicApiKeyClientConfig,
  KanaGroupClientConfig,
  KanaGroupClientFullConfig,
  KanaGroupTokenClientConfig,
} from './KanaGroupClientConfig';
import { request } from './request';
import { Consumption, Entitlement, Feature, Package, Group } from './types';
import { unique } from './utils/unique';

const maxRetries = 3;

const defaultConfigOptions = {
  endpoint: 'https://client-api.usekana.com/graphql',
  version: '0.1',
  retry: (error: Error, retryNumber: number) => {
    return error instanceof NetworkError && retryNumber < maxRetries;
  },
};

export class KanaGroupClient {
  public readonly config: KanaGroupClientFullConfig;

  private readonly gqlSdk: GQLSdk;
  private _groupCached = false;
  private _group?: Group = undefined;
  private _groupSubscribedPackages: Package[] = [];
  private _groupSubscribedFeatures: Feature[] = [];
  private _groupFeatureConsumptions: Map<string, Consumption> = new Map();

  constructor(config: KanaGroupClientConfig) {
    if (!config) {
      throw new EmptyArgumentError('config');
    }

    if ((config as KanaGroupTokenClientConfig).groupToken) {
      this.config = {
        ...defaultConfigOptions,
        ...(config as KanaGroupTokenClientConfig),
        type: 'GroupToken',
      };
    } else if ((config as KanaPublicApiKeyClientConfig).apiKey) {
      if (!(config as KanaPublicApiKeyClientConfig).groupId) {
        throw new Error(
          'Kana config error, "groupId" is required when "apiKey" is used.',
        );
      }

      this.config = {
        ...defaultConfigOptions,
        ...(config as KanaPublicApiKeyClientConfig),
        type: 'PublicApiKey',
      };
    } else {
      throw new Error(
        'Kana config error, "groupToken" or "apiKey" is required for client initialization.',
      );
    }

    this.gqlSdk = createGQLSdk(this.config);
  }

  async resetCache() {
    await request<void, RequestError>(this.config, async () => {
      const groupCache = await this.gqlSdk.CurrentGroup();
      this.updateGroupFields(groupCache);
    });
  }

  async getGroup() {
    return request<Group | undefined, RequestError>(this.config, async () => {
      await this.initGroupCache();
      return this._group;
    });
  }

  async getSubscribedPackages() {
    return request<Package[], RequestError>(this.config, async () => {
      await this.initGroupCache();
      return this._groupSubscribedPackages;
    });
  }

  async getSubscribedFeatures() {
    return request<Feature[], RequestError>(this.config, async () => {
      await this.initGroupCache();
      return this._groupSubscribedFeatures;
    });
  }

  async canUseFeature(featureId: string, delta?: number) {
    return request<Entitlement, RequestError>(this.config, async () => {
      await this.initGroupCache();

      const feature = this._groupSubscribedFeatures.find(
        (f) => f.id === featureId,
      );

      if (feature) {
        if (feature.type === FeatureType.Binary) {
          return {
            access: true,
            reason:
              'The group has subscribed to a package with this binary feature.',
          };
        } else if (feature.type === FeatureType.Consumable) {
          const consumption = this._groupFeatureConsumptions.get(featureId);

          if (consumption) {
            const calculatedUsed = delta
              ? delta - 1 + consumption.used
              : consumption.used;

            const access =
              // unlimited budget or overage is allowed
              consumption.budget === null || consumption.overageEnabled
                ? true
                : calculatedUsed < consumption.budget;

            return {
              access,
              consumption,
              reason: access
                ? 'The group has a subcription to a package with this consumable feature and either has an allowance remaining or overage is enabled.'
                : 'The group has no remanining allowance of this feature and overage is not enabled.',
            };
          }
        }
      }

      return {
        access: false,
        reason: 'The group has no active subscription to the feature.',
      };
    });
  }

  private async initGroupCache() {
    if (!this._groupCached) {
      const cache = await this.gqlSdk.CurrentGroup();
      this.updateGroupFields(cache);
      this._groupCached = true;
    }
  }

  private updateGroupFields(cache: CurrentGroupQuery) {
    const currentGroup = cache.currentGroup;

    this._group = {
      id: currentGroup.id,
      email: currentGroup.email,
      name: currentGroup.name,
      metadata: currentGroup.metadata,
    };

    this._groupSubscribedPackages = unique(
      currentGroup.subscriptions.map((sub) => ({
        id: sub.package.id,
        name: sub.package.name,
        isAddon: sub.package.isAddon,
        metadata: sub.package.metadata,
      })),
      (p) => p.id,
    );

    this._groupSubscribedFeatures = unique(
      currentGroup.subscriptions.flatMap((sub) => sub.package.features),
      (f) => f.id,
    ).map((f) => ({
      id: f.id,
      name: f.name,
      type: f.type,
      metadata: f.metadata,
      unitLabel: f.unitLabel,
      unitLabelPlural: f.unitLabelPlural,
    }));

    const featureConsumptions = currentGroup.subscriptions
      .flatMap((sub) =>
        sub.package.features.flatMap((f) => ({
          feature: f,
          consumption: f.consumption,
        })),
      )
      .reduce((agg, fetCon) => {
        const item = agg[fetCon.feature.id];
        const consumptions = item?.consumptions || [];
        const consumption = fetCon.consumption as
          | Consumption
          | undefined
          | null;
        if (consumption) {
          consumptions.push(consumption);
        }

        agg[fetCon.feature.id] = {
          feature: fetCon.feature,
          consumptions: consumptions,
        };

        return agg;
      }, {} as Record<string, { feature: Feature; consumptions: Consumption[] }>);

    this._groupFeatureConsumptions = new Map();
    for (const featureId of Object.keys(featureConsumptions)) {
      const fetCons = featureConsumptions[featureId];

      const aggConsumption = {
        budget: fetCons.consumptions.find((c) => c.budget === null)
          ? null
          : fetCons.consumptions.reduce((agg, c) => agg + (c.budget ?? 0), 0),
        used: fetCons.consumptions.reduce((agg, c) => agg + c.used, 0),
        overageEnabled: fetCons.consumptions.reduce(
          (agg, c) => agg || c.overageEnabled,
          false,
        ),
      };

      this._groupFeatureConsumptions.set(featureId, aggConsumption);
    }
  }
}
