import { Context } from "aws-lambda";
import { DynamoDB } from "aws-sdk";
import { DependencyResolveError, NotFoundError } from "../errors";
import {
  createPackageHash,
  IParsedPackageTarget,
  parsePackageTarget,
} from "../helpers";
import { initCommonMiddlewares } from "../middlewares";
import { IScriptDependency } from "./global";

const toposort = require("toposort");

interface IEvent {
  package: {
    name: string;
    namespace?: string;
  };
  dependencies: IScriptDependency[];
}

interface IDependencyNode {
  hash: string;
  package: IParsedPackageTarget;
  children: IDependencyNode[];
  level?: number;
}

type IResult = IParsedPackageTarget[];

const { TABLE_NAME } = process.env;

const ddb = new DynamoDB.DocumentClient();

/**
 * @Todo: [improvement] Use data loader to create batch reads and avoid duplication reads.
 */
async function awsHandler(event: IEvent, context: Context): Promise<IResult> {
  const {
    package: { name, namespace },
    dependencies = null,
  } = event;

  if (!dependencies || !dependencies.length) {
    return [];
  }

  const rootNode = {
    package: {
      name,
      namespace,
    },
    hash: createPackageHash({ name, namespace }),
    children: await createTree(dependencies),
  } as IDependencyNode;

  const hashList = [];
  const nodeList = [];

  function resolveNode(node: IDependencyNode, level: number) {
    nodeList.push({ ...node, level });
    if (!node.children.length) {
      return;
    }
    const depends = node.children.map((child) => child.hash);
    hashList.push([node.hash, ...depends]);
    node.children.forEach((child) => resolveNode(child, level + 1));
  }

  resolveNode(rootNode, 0);

  const sortedList = toposort(hashList).reverse();

  const index = normalize(nodeList);

  const result = sortedList.map((hash) => {
    const node = index[hash] as IDependencyNode;
    return node.package;
  });

  result.pop(); // remove root package

  return result;
}

export function normalize(nodes: IDependencyNode[]) {
  const index = {};
  const solvedList: IDependencyNode[] = [];

  nodes.forEach((node) => {
    if (index[node.hash] && index[node.hash].level > node.level) {
      const indexOfNode = solvedList.indexOf(index[node.hash]);
      solvedList[indexOfNode] = node;
      index[node.hash] = node;
    } else if (!index[node.hash]) {
      // add
      index[node.hash] = node;
      solvedList.push(node);
    }
  });

  return index;
}

export async function createTree(
  depends: IScriptDependency[],
): Promise<IDependencyNode[]> {
  const tasks = depends.map(async (dependency) => {
    const parsedTarget = parsePackageTarget(dependency.name);
    const children = await getDependencies(
      parsedTarget.name,
      dependency.version,
      parsedTarget.namespace,
    );

    return {
      hash: createPackageHash({
        name: parsedTarget.name,
        namespace: parsedTarget.namespace,
      }),
      package: {
        name: parsedTarget.name,
        namespace: parsedTarget.namespace,
        version: dependency.version,
      },
      children: children.length ? await createTree(children) : [],
    } as IDependencyNode;
  });

  return await Promise.all(tasks);
}

export async function getDependencies(
  name: string,
  version: string,
  namespace?: string,
): Promise<IScriptDependency[]> {
  const packageHash = createPackageHash({ name, namespace });

  const { Items } = await ddb
    .query({
      TableName: TABLE_NAME,
      KeyConditions: {
        packageHash: {
          AttributeValueList: [packageHash],
          ComparisonOperator: "EQ",
        },
        version: {
          AttributeValueList: [version],
          ComparisonOperator: "EQ",
        },
      },
      AttributesToGet: ["dependencies"],
    })
    .promise();

  if (!Items || !Items.length) {
    throw new NotFoundError(
      `Dependency '${namespace}/${name}=${version}' not found`,
    );
  }
  const [item] = Items;
  return item.dependencies || [];
}

export const handler = initCommonMiddlewares(awsHandler, {
  error: new DependencyResolveError("Couldn't resolve dependencies."),
});
