import { omit, sum } from 'rambda';
import { z } from 'zod';
import { asc, byNum, desc } from 'sort-lib';
import { exists, merge } from '../../shared/utils.js';
import { noGroup } from '../../shared/work-item-utils.js';
import { configForCollection } from '../config.js';
import { collectionAndProjectInputs, queryPeriodInputParser, timezone, queryRangeFilter, } from './helpers.js';
import { WorkItemModel } from './mongoose-models/WorkItem.js';
const apiShapeToModelShape = (collectionName, workItem) => ({
    id: workItem.id,
    collectionName,
    project: workItem.fields['System.TeamProject'],
    workItemType: workItem.fields['System.WorkItemType'],
    state: workItem.fields['System.State'],
    changeDate: new Date(workItem.fields['System.ChangedDate']),
    createdDate: new Date(workItem.fields['System.CreatedDate']),
    title: workItem.fields['System.Title'],
    description: workItem.fields['System.Description'],
    closedDate: workItem.fields['Microsoft.VSTS.Common.ClosedDate']
        ? new Date(workItem.fields['Microsoft.VSTS.Common.ClosedDate'])
        : undefined,
    stateChangeDate: workItem.fields['Microsoft.VSTS.Common.StateChangeDate']
        ? new Date(workItem.fields['Microsoft.VSTS.Common.StateChangeDate'])
        : undefined,
    priority: workItem.fields['Microsoft.VSTS.Common.Priority'] || undefined,
    severity: workItem.fields['Microsoft.VSTS.Common.Severity'] || undefined,
    fields: workItem.fields,
    url: workItem.url,
    relations: workItem.relations,
});
export const bulkUpsertWorkItems = (collectionName) => (workItems) => WorkItemModel.bulkWrite(workItems.map(workItem => ({
    updateOne: {
        filter: { collectionName, id: workItem.id },
        update: { $set: apiShapeToModelShape(collectionName, workItem) },
        upsert: true,
    },
})));
const field = (fieldName) => ({
    $getField: {
        field: { $literal: fieldName },
        input: '$fields',
    },
});
const formatUrl = (wi) => ({
    ...wi,
    url: wi.url.replace('_apis/wit/workItems/', '_workitems/edit/'),
});
const sanitizeFieldName = (field) => field.replaceAll(/\s/g, '_').replaceAll('.', '_').replaceAll('$', '_');
const applyAdditionalFilters = (collectionName, additionalFilters) => {
    if (!additionalFilters)
        return [];
    return Object.entries(additionalFilters).flatMap(([label, value]) => {
        var _a, _b;
        const configForField = (_b = (_a = configForCollection(collectionName)) === null || _a === void 0 ? void 0 : _a.workitems.filterBy) === null || _b === void 0 ? void 0 : _b.find(f => f.label === label);
        if (!configForField)
            return [];
        if (configForField.fields.length === 1 && !configForField.delimiter) {
            // Single field, no delimiter, so exact match is sufficient
            return [{ $match: { $expr: { $in: [field(configForField.fields[0]), value] } } }];
        }
        return [
            // Concat the fields
            {
                $addFields: {
                    [`${sanitizeFieldName(label)}-temp-1`]: {
                        $concat: configForField.fields.flatMap(f => [
                            { $ifNull: [field(f), ''] },
                            configForField.delimiter,
                        ]),
                    },
                },
            },
            // Split by the delimiter
            {
                $addFields: {
                    [`${sanitizeFieldName(label)}-temp-2`]: {
                        $split: [`$${sanitizeFieldName(label)}-temp-1`, configForField.delimiter],
                    },
                },
            },
            // Filter out empty fields
            {
                $addFields: {
                    [`${sanitizeFieldName(label)}`]: {
                        $filter: {
                            input: `$${sanitizeFieldName(label)}-temp-2`,
                            as: 'part',
                            cond: { $ne: ['$$part', ''] },
                        },
                    },
                },
            },
            // Check if value exists
            { $match: { [sanitizeFieldName(label)]: { $in: value } } },
        ];
    });
};
const getWorkItemsOfType = (collectionName, project, workItemTypeName) => {
    var _a;
    return [
        {
            $match: {
                collectionName,
                project,
                workItemType: workItemTypeName,
                state: { $nin: ((_a = configForCollection(collectionName)) === null || _a === void 0 ? void 0 : _a.workitems.ignoreStates) || [] },
            },
        },
    ];
};
const startDateFieldForNew = (workItemTypeConfig) => (workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.type.toLowerCase().includes('bug'))
    ? ['System.CreatedDate']
    : workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.startDate;
const createDateField = (fieldName, fields) => ({
    $addFields: { [fieldName]: { $min: (fields || []).map(field) } },
});
const ensureDateInRange = (newFieldName, fields, queryPeriod) => [
    // Get the min date based on the fields
    createDateField(newFieldName, fields),
    // Ensure it's within range
    { $match: { [newFieldName]: queryRangeFilter(queryPeriod) } },
];
const groupByField = (workItemTypeConfig) => (workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.groupByField) ? field(workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.groupByField) : noGroup;
const groupByDate = (dateField, queryPeriod) => ({
    $dateToString: {
        date: `$${dateField}`,
        timezone: timezone(queryPeriod),
        format: '%Y-%m-%d',
    },
});
const newWorkItemsBaseInput = {
    ...collectionAndProjectInputs,
    additionalFilters: z.record(z.string(), z.array(z.string())),
};
const newWorkItemsSummaryInput = {
    ...newWorkItemsBaseInput,
    queryPeriod: queryPeriodInputParser,
};
export const newWorkItemsSummaryInputParser = z.object(newWorkItemsSummaryInput);
const newWorkItemsForWorkItemType = ({ collectionName, project, workItemType, queryPeriod, additionalFilters, }) => {
    var _a;
    const collectionConfig = configForCollection(collectionName);
    const workItemTypeConfig = (_a = collectionConfig === null || collectionConfig === void 0 ? void 0 : collectionConfig.workitems.types) === null || _a === void 0 ? void 0 : _a.find(t => t.type === workItemType);
    const pipeline = [
        ...getWorkItemsOfType(collectionName, project, workItemType),
        ...ensureDateInRange('startDate', startDateFieldForNew(workItemTypeConfig), queryPeriod),
        ...applyAdditionalFilters(collectionName, additionalFilters),
        {
            $group: {
                _id: {
                    group: groupByField(workItemTypeConfig),
                    date: groupByDate('startDate', queryPeriod),
                },
                count: { $sum: 1 },
            },
        },
        {
            // Nest the startDate inside the group
            $group: { _id: '$_id.group', count: { $push: { k: '$_id.date', v: '$count' } } },
        },
        {
            // And convert to object to reduce size
            $project: { _id: 1, counts: { $arrayToObject: '$count' } },
        },
    ];
    return WorkItemModel.aggregate(pipeline).then(result => Object.fromEntries(result
        .sort(desc(byNum(r => sum(Object.values(r.counts)))))
        .sort(asc(byNum(r => { var _a; return ((_a = collectionConfig === null || collectionConfig === void 0 ? void 0 : collectionConfig.projects[0].environments) === null || _a === void 0 ? void 0 : _a.indexOf(r._id)) || -1; })))
        .map((r) => [r._id, r.counts])));
};
export const newWorkItemsSummary = async (options) => {
    const collConfig = configForCollection(options.collectionName);
    if (!collConfig)
        return {};
    const { types } = collConfig.workitems;
    if (!types)
        return {};
    return Promise.all(types.map(type => newWorkItemsForWorkItemType({ ...options, workItemType: type.type }).then(result => {
        if (Object.keys(result).length === 0)
            return {};
        return { [type.type]: result };
    }))).then(result => result.reduce(merge, {}));
};
export const newWorkItemsListForGroupInputParser = z.object({
    ...newWorkItemsBaseInput,
    queryPeriod: queryPeriodInputParser,
    workItemType: z.string(),
    groupName: z.string(),
});
export const newWorkItemsListForGroup = async ({ collectionName, project, workItemType, groupName, additionalFilters, queryPeriod, }) => {
    var _a;
    const collectionConfig = configForCollection(collectionName);
    const workItemTypeConfig = (_a = collectionConfig === null || collectionConfig === void 0 ? void 0 : collectionConfig.workitems.types) === null || _a === void 0 ? void 0 : _a.find(t => t.type === workItemType);
    const pipeline = [
        ...getWorkItemsOfType(collectionName, project, workItemType),
        ...ensureDateInRange('startDate', startDateFieldForNew(workItemTypeConfig), queryPeriod),
        ...applyAdditionalFilters(collectionName, additionalFilters),
        ...(groupName === noGroup || !(workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.groupByField)
            ? []
            : [
                {
                    $match: {
                        $expr: { $eq: [field(workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.groupByField), groupName] },
                    },
                },
            ]),
        {
            $project: {
                id: 1,
                title: 1,
                date: '$startDate',
                url: 1,
            },
        },
    ];
    return WorkItemModel.aggregate(pipeline).then(wis => wis.map(omit(['_id'])).map(formatUrl));
};
export const newWorkItemsListForDateInputParser = z.object({
    ...newWorkItemsBaseInput,
    date: z.date(),
    timeZone: z.string(),
});
export const newWorkItemsListForDate = async ({ collectionName, project, additionalFilters, date, timeZone, }) => {
    const collectionConfig = configForCollection(collectionName);
    const startOfRange = new Date(date);
    startOfRange.setHours(0, 0, 0, 0);
    const endOfRange = new Date(startOfRange);
    endOfRange.setDate(startOfRange.getDate() + 1);
    const queryPeriod = [startOfRange, endOfRange, timeZone];
    const results = await Promise.all(((collectionConfig === null || collectionConfig === void 0 ? void 0 : collectionConfig.workitems.types) || []).map(workItemType => {
        const pipeline = [
            ...getWorkItemsOfType(collectionName, project, workItemType.type),
            ...ensureDateInRange('startDate', startDateFieldForNew(workItemType), queryPeriod),
            ...applyAdditionalFilters(collectionName, additionalFilters),
            {
                $project: {
                    id: 1,
                    title: 1,
                    date: '$startDate',
                    url: 1,
                    type: '$workItemType',
                    group: workItemType.groupByField ? field(workItemType.groupByField) : noGroup,
                },
            },
        ];
        return WorkItemModel.aggregate(pipeline);
    }));
    return results
        .flat()
        .map(omit(['_id']))
        .map(formatUrl);
};
export const workItemForTooltipInputParser = z.object({
    collectionName: z.string(),
    id: z.number(),
});
const getDateFieldValue = (fields, wi) => {
    if (!fields)
        return;
    return new Date(Math.min(...fields
        .map(f => wi.fields[f])
        .filter(exists)
        .map(d => new Date(d).getTime())));
};
export const workItemForTooltip = async ({ collectionName, id, }) => {
    var _a;
    const match = await WorkItemModel.findOne({ collectionName, id }).lean();
    const collectionConfig = configForCollection(collectionName);
    const workItemTypeConfig = (_a = collectionConfig === null || collectionConfig === void 0 ? void 0 : collectionConfig.workitems.types) === null || _a === void 0 ? void 0 : _a.find(t => t.type === (match === null || match === void 0 ? void 0 : match.workItemType));
    if (!match)
        return;
    const startDate = getDateFieldValue(workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.startDate, match);
    const endDate = getDateFieldValue(workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.endDate, match);
    const devDoneDate = getDateFieldValue(workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.devCompletionDate, match);
    const cycleTime = startDate && endDate && endDate.getTime() - startDate.getTime();
    const clt = devDoneDate && endDate && endDate.getTime() - devDoneDate.getTime();
    return {
        id: match.id,
        title: match.title,
        state: match.state,
        group: (workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.groupByField)
            ? match.fields[workItemTypeConfig.groupByField]
            : undefined,
        priority: match.fields['Microsoft.VSTS.Common.Priority'],
        severity: match.fields['Microsoft.VSTS.Common.Severity'],
        rca: workItemTypeConfig === null || workItemTypeConfig === void 0 ? void 0 : workItemTypeConfig.rootCause.map(rcaField => match.fields[rcaField]).filter(exists),
        cycleTime,
        clt,
        startDate,
    };
};
