import * as cdk from "@aws-cdk/core";
import * as kinesis from "@aws-cdk/aws-kinesis";
import * as lambda from "@aws-cdk/aws-lambda";
import * as lambdaEventSources from "@aws-cdk/aws-lambda-event-sources";
import { App } from "./App";
import { Function as Fn, FunctionProps, FunctionDefinition } from "./Function";
import { Permissions } from "./util/permission";

/////////////////////
// Interfaces
/////////////////////

export interface KinesisStreamProps {
  readonly kinesisStream?: kinesis.IStream | kinesis.StreamProps;
  readonly consumers?: {
    [consumerName: string]: FunctionDefinition | KinesisStreamConsumerProps;
  };
  readonly defaultFunctionProps?: FunctionProps;
}

export interface KinesisStreamConsumerProps {
  readonly function: FunctionDefinition;
  readonly consumerProps?: lambdaEventSources.KinesisEventSourceProps;
}

/////////////////////
// Construct
/////////////////////

export class KinesisStream extends cdk.Construct {
  public readonly kinesisStream: kinesis.IStream;
  private functions: { [consumerName: string]: Fn };
  private readonly permissionsAttachedForAllConsumers: Permissions[];
  private readonly defaultFunctionProps?: FunctionProps;

  constructor(scope: cdk.Construct, id: string, props?: KinesisStreamProps) {
    super(scope, id);

    const root = scope.node.root as App;
    const { kinesisStream, consumers, defaultFunctionProps } = props || {};
    this.functions = {};
    this.permissionsAttachedForAllConsumers = [];
    this.defaultFunctionProps = defaultFunctionProps;

    ////////////////////
    // Create Stream
    ////////////////////

    if (cdk.Construct.isConstruct(kinesisStream)) {
      this.kinesisStream = kinesisStream as kinesis.IStream;
    } else {
      const kinesisStreamProps = (kinesisStream || {}) as kinesis.StreamProps;
      this.kinesisStream = new kinesis.Stream(this, "Stream", {
        streamName: root.logicalPrefixedName(id),
        ...kinesisStreamProps,
      });
    }

    ///////////////////////////
    // Create Consumers
    ///////////////////////////

    if (consumers) {
      Object.keys(consumers).forEach((consumerName: string) =>
        this.addConsumer(this, consumerName, consumers[consumerName])
      );
    }
  }

  public get streamArn(): string {
    return this.kinesisStream.streamArn;
  }

  public get streamName(): string {
    return this.kinesisStream.streamName;
  }

  public addConsumers(
    scope: cdk.Construct,
    consumers: {
      [consumerName: string]: FunctionDefinition | KinesisStreamConsumerProps;
    }
  ): void {
    Object.keys(consumers).forEach((consumerName: string) => {
      this.addConsumer(scope, consumerName, consumers[consumerName]);
    });
  }

  public attachPermissions(permissions: Permissions): void {
    Object.values(this.functions).forEach((fn) =>
      fn.attachPermissions(permissions)
    );
    this.permissionsAttachedForAllConsumers.push(permissions);
  }

  public attachPermissionsToConsumer(
    consumerName: string,
    permissions: Permissions
  ): void {
    if (!this.functions[consumerName]) {
      throw new Error(
        `The "${consumerName}" consumer was not found in the "${this.node.id}" KinesisStream.`
      );
    }

    this.functions[consumerName].attachPermissions(permissions);
  }

  public getFunction(consumerName: string): Fn | undefined {
    return this.functions[consumerName];
  }

  private addConsumer(
    scope: cdk.Construct,
    consumerName: string,
    consumer: FunctionDefinition | KinesisStreamConsumerProps
  ): Fn {
    // normalize consumer
    let consumerFunction, consumerProps;
    if ((consumer as KinesisStreamConsumerProps).function) {
      consumer = consumer as KinesisStreamConsumerProps;
      consumerFunction = consumer.function;
      consumerProps = consumer.consumerProps;
    } else {
      consumerFunction = consumer as FunctionDefinition;
    }
    consumerProps = {
      startingPosition: lambda.StartingPosition.LATEST,
      ...(consumerProps || {}),
    };

    // create function
    const fn = Fn.fromDefinition(
      scope,
      consumerName,
      consumerFunction,
      this.defaultFunctionProps,
      `The "defaultFunctionProps" cannot be applied if an instance of a Function construct is passed in. Make sure to define all the consumers using FunctionProps, so the KinesisStream construct can apply the "defaultFunctionProps" to them.`
    );
    this.functions[consumerName] = fn;

    // create event source
    const eventSource = new lambdaEventSources.KinesisEventSource(
      this.kinesisStream,
      consumerProps
    );
    fn.addEventSource(eventSource);

    // attach permissions
    this.permissionsAttachedForAllConsumers.forEach((permissions) => {
      fn.attachPermissions(permissions);
    });

    return fn;
  }
}
