import * as md5 from "md5";
import * as moment from "moment";
import { NAME_MAX_LENGTH, NAME_MIN_LENGTH } from "./base";
import { LambdaInvoker } from "./LambdaInvoker";

interface ISearchQueryArgs {
  term: string;
  category?: string;
  platform?: {
    os: string;
    dist: string;
    codename: string;
  };
}

interface IPackageActivitiesArgs {
  name: string;
  namespace?: string;
}

class PackageSearchInvoker extends LambdaInvoker {
  public resolvePayload(args: ISearchQueryArgs) {
    const { term } = args;
    const invocationArgs = {
      indexName: "packages",
      type: "release",
      body: this.getInitialDSL(term) as any,
    };

    const categoryMust = [];
    let platformMust = [];

    if (args.platform) {
      const { codename, dist, os } = args.platform;
      platformMust = [
        { match: { "platform.os.keyword": os } },
        { match: { "platform.dist.keyword": dist } },
        { match: { "platform.codename.keyword": codename } },
      ];
    }

    if (args.category) {
      const { category } = args;
      invocationArgs.body.query.bool.must.push({
        match: { "category.keyword": category },
      });
    }

    if (platformMust.length) {
      invocationArgs.body.query.bool.must[0].bool.should.push({
        bool: {
          must_not: [{ exists: { field: "platform" } }],
        },
      });

      invocationArgs.body.query.bool.must[0].bool.should.push({
        bool: { must: platformMust },
      });
    }
    return JSON.stringify(invocationArgs);
  }

  public resolveResponse(response: any) {
    const {
      aggregations,
      _info: { took },
    } = response;
    const { buckets } = aggregations.package;

    const items = buckets.map((bucket) => {
      const {
        packages: {
          hits: {
            hits: [hit],
          },
        },
      } = bucket;

      // @Todo: Implement and index the actual release date, currently we provide mock implementation.
      return {
        release: new Date().toISOString(),
        ...hit._source,
      };
    });

    return {
      items,
      queryTime: took,
    };
  }

  private getInitialDSL(term) {
    return {
      size: 0,
      query: {
        bool: {
          must: [
            {
              bool: {
                should: [
                  // no platform should always display as result without platform filter
                ],
                minimum_should_match: 1,
              },
            },
          ],
          should: [
            {
              match: {
                description: { query: term, boost: 1.5 },
              },
            },
            { match: { target: { query: term, boost: 2 } } },
            { match: { readme: { query: term, boost: 1 } } },
          ],
          minimum_should_match: 1,
        },
      },
      aggs: {
        package: {
          terms: {
            field: "target.keyword",
            order: {
              top_score: "desc",
            },
          },
          aggs: {
            top_score: {
              max: {
                script: "_score",
              },
            },
            packages: {
              top_hits: {
                _source: [
                  "name",
                  "namespace",
                  "description",
                  "version",
                  "category",
                ],
                size: 1,
                sort: [
                  {
                    versionNum: {
                      order: "desc",
                    },
                  },
                ],
              },
            },
          },
        },
      },
    };
  }
}
class PackageActivitiesStatsInvoker extends LambdaInvoker {
  public resolvePayload(args: IPackageActivitiesArgs[]) {
    const invocationArgs = {
      indexName: "activities",
      type: "activity",
      body: this.getInitialDSL(),
    };

    invocationArgs.body.query.bool.should = args.map(({ name, namespace }) => ({
      match: {
        "packageHash.keyword": md5((namespace || "none") + name),
      },
    }));

    return JSON.stringify(invocationArgs);
  }

  public resolveResponse(response: any, args: IPackageActivitiesArgs[]) {
    const {
      aggregations,
      _info: { took },
    } = response;
    const {
      package_releases: { buckets: prBuckets },
      package_executions: { buckets: peBuckets },
    } = aggregations;

    let packageStats = args.reduce((packageStats, { name, namespace }) => {
      const packageHash = md5((namespace || "none") + name);
      packageStats[packageHash] = { name, namespace };
      return packageStats;
    }, {});

    packageStats = peBuckets.reduce(
      (packageStats, { activities: { buckets }, key: packageHash }) => {
        const stats = buckets.reduce((stats, { key, doc_count }) => {
          stats[key] = doc_count;
          return stats;
        }, {});

        packageStats[packageHash] = {
          ...packageStats[packageHash],
          ...stats,
        };

        return packageStats;
      },
      packageStats,
    );

    prBuckets.forEach(
      ({
        key: packageHash,
        releases: {
          releases: {
            hits: {
              hits: [hit],
            },
          },
        },
      }) => {
        if (packageStats[packageHash]) {
          packageStats[packageHash] = {
            ...packageStats[packageHash],
            latestRelease: hit._source.datetime,
          };
        }
      },
    );

    const items = Object.keys(packageStats).map((packageHash) => {
      const {
        latestRelease,
        SUCCESSFUL,
        FAILED,
        name,
        namespace,
      } = packageStats[packageHash];
      return {
        name,
        packageHash,
        namespace,
        latestRelease,
        successful: SUCCESSFUL || 0,
        failed: FAILED || 0,
      };
    });

    return {
      items,
      queryTime: took,
    };
  }

  private getInitialDSL() {
    return {
      size: 0,
      query: { bool: { should: [] } },
      aggs: {
        package_executions: {
          terms: { field: "packageHash.keyword" },
          aggs: { activities: { terms: { field: "type.keyword" } } },
        },
        package_releases: {
          terms: { field: "packageHash.keyword" },
          aggs: {
            releases: {
              filter: { term: { type: "publish" } },
              aggs: {
                top_hits: { max: { field: "datetime" } },
                releases: {
                  top_hits: {
                    _source: ["datetime", "packageHash"],
                    sort: [{ datetime: { order: "desc" } }],
                    size: 1,
                  },
                },
              },
            },
          },
        },
      },
    };
  }
}

class PackageActivitiesTimelineInvoker extends LambdaInvoker {
  public resolvePayload(args: any) {
    const { name, namespace } = args;
    const hash = md5((namespace || "none") + name);
    const start = moment()
      .subtract(6, "days")
      .toDate()
      .getTime();
    const end = new Date().getTime();

    const invocationArgs = {
      indexName: "activities",
      type: "activity",
      body: this.getInitialDSL(hash, start, end),
    };
    return JSON.stringify(invocationArgs);
  }

  public resolveResponse(response: any, args: any) {
    const { aggregations } = response;
    const {
      time: { buckets },
    } = aggregations;

    return buckets.map((bucket) => {
      const stats = bucket.type.buckets.reduce((stats, bucket) => {
        stats[bucket.key] = bucket.doc_count;
        return stats;
      }, {});

      return {
        datetime: bucket.key_as_string,
        successful: stats.SUCCESSFUL || 0,
        failed: stats.FAILED || 0,
      };
    });
  }

  private getInitialDSL(hash: string, start: number, end: number) {
    return {
      aggs: {
        time: {
          date_histogram: {
            field: "datetime",
            interval: "24h",
            time_zone: "UTC",
            min_doc_count: 1,
          },
          aggs: {
            type: {
              terms: {
                field: "type.keyword",
                size: 5,
                order: {
                  _count: "desc",
                },
              },
            },
          },
        },
      },
      size: 0,
      _source: {
        excludes: [],
      },
      stored_fields: ["*"],
      script_fields: {},
      docvalue_fields: [
        {
          field: "datetime",
          format: "date_time",
        },
      ],
      query: {
        bool: {
          must: [
            {
              range: {
                datetime: {
                  gte: start,
                  lte: end,
                  format: "epoch_millis",
                },
              },
            },
            {
              match: {
                "packageHash.keyword": hash,
              },
            },
            {
              bool: {
                minimum_should_match: 1,
                should: [
                  {
                    match_phrase: {
                      "type.keyword": "SUCCESSFUL",
                    },
                  },
                  {
                    match_phrase: {
                      "type.keyword": "FAILED",
                    },
                  },
                ],
              },
            },
          ],
          filter: [
            {
              match_all: {},
            },
          ],
          should: [],
          must_not: [],
        },
      },
    };
  }
}

const activitiesTimeline = new PackageActivitiesTimelineInvoker(
  "search",
  "search",
  {
    type: "object",
    properties: {
      name: { type: "string" },
      namespace: { type: "string" },
    },
    required: ["name"],
  },
);

const search = new PackageSearchInvoker("search", "search", {
  type: "object",
  properties: {
    term: { type: "string" },
    category: { type: "string" },
    platform: {
      type: "object",
      properties: {
        os: { type: "string" },
        dist: { type: "string" },
        codename: { type: "string" },
      },
      required: ["os", "dist", "codename"],
    },
  },
  required: ["term"],
});

const activitiesStats = new PackageActivitiesStatsInvoker("search", "search", {
  type: "array",
  items: {
    type: "object",
    properties: {
      name: {
        type: "string",
        format: "name",
        maxLength: NAME_MAX_LENGTH,
        minLength: NAME_MIN_LENGTH,
      },
      namespace: {
        type: "string",
        format: "name",
        maxLength: NAME_MAX_LENGTH,
        minLength: NAME_MIN_LENGTH,
      },
    },
    required: ["name"],
  },
  minItems: 1,
});

export { search, activitiesStats, activitiesTimeline };
