/**
 * Badge recalculation endpoint.
 *
 * Rebuilds badge values for workflow by computing a complete badgeValue object
 * and replacing the entire `value` field in steedos_keyvalues (instead of
 * dot-notation partial updates). This removes stale keys left behind when an
 * app is unbound from a category.
 *
 * Route: GET /api/workflow/badge-recalc-execute
 */
const objectql = require('@steedos/objectql');
const { getObject } = objectql;
const { getCollection } = require('../../main/default/utils/collection');
const _ = require('underscore');
const { count_instance_tasks } = require('../../main/default/manager/instance_tasks_manager');

/**
 * Compare two badge value objects, treating keys with value 0 as equivalent
 * to missing keys. For example, `{ workflow: 0 }` and `{ oa: 0, workflow: 0 }`
 * are considered equal because all non-zero values match.
 * Returns true if the objects are semantically equal.
 */
function badgeValuesEqual(a, b) {
    const objA = a || {};
    const objB = b || {};
    const allKeys = new Set([...Object.keys(objA), ...Object.keys(objB)]);
    for (const key of allKeys) {
        if ((objA[key] || 0) !== (objB[key] || 0)) {
            return false;
        }
    }
    return true;
}

/**
 * Recalculate badge for a single user across all their spaces.
 * Returns per-space results with before/after comparison.
 */
async function recalcUserBadge(userId, dryRun, categoriesCol, spaceUsersCol, options) {
    const results = [];
    const { includeNames } = options || {};

    const userSpaces = await spaceUsersCol.find(
        { user: userId, user_accepted: true },
        { fields: { space: 1 } }
    ).toArray();

    for (const userSpace of userSpaces) {
        const spaceId = userSpace.space;
        const badgeValue = {};

        // 1. Per-app badge from categories
        const categories = await categoriesCol.find(
            { space: spaceId, app: { $ne: null } },
            { fields: { app: 1 } }
        ).toArray();

        if (categories.length > 0) {
            const appCategories = _.groupBy(categories, 'app');

            for (const appName in appCategories) {
                if (!appName) continue;

                const categoryIds = _.pluck(appCategories[appName], '_id');
                if (categoryIds.length === 0) continue;

                const categoryBadge = await count_instance_tasks({
                    handler: userId,
                    is_finished: false,
                    space: spaceId,
                    category: { $in: categoryIds },
                });

                badgeValue[appName] = categoryBadge;
            }
        }

        // 2. Total workflow badge for this space
        const taskCount = await count_instance_tasks({
            handler: userId,
            is_finished: false,
            space: spaceId,
        });
        badgeValue.workflow = taskCount;

        // 3. Read current value from DB
        const existing = await getObject('steedos_keyvalues').find({
            filters: [['user', '=', userId], ['space', '=', spaceId], ['key', '=', 'badge']],
            fields: ['_id', 'value']
        });

        const before = existing.length > 0 ? (existing[0].value || {}) : {};
        const hasDiff = !badgeValuesEqual(before, badgeValue);

        // 4. Write if not dryRun
        if (!dryRun && hasDiff) {
            if (existing.length > 0) {
                await getObject('steedos_keyvalues').update(existing[0]._id, { value: badgeValue });
            } else {
                await getObject('steedos_keyvalues').insert({
                    user: userId,
                    space: spaceId,
                    key: 'badge',
                    value: badgeValue
                });
            }
        }

        const record = {
            userId,
            space: spaceId,
            before,
            after: badgeValue,
            hasDiff
        };

        if (includeNames) {
            record.spaceName = spaceId;  // placeholder, resolved below
        }

        results.push(record);
    }

    // Global cross-space badge record (space: null) — mirrors push_manager.get_badge L487-L503
    const totalBadge = results.reduce((sum, r) => sum + (r.after.workflow || 0), 0);
    const globalBadgeValue = { workflow: totalBadge };

    const existingGlobal = await getObject('steedos_keyvalues').find({
        filters: [['user', '=', userId], ['space', '=', null], ['key', '=', 'badge']],
        fields: ['_id', 'value']
    });

    const globalBefore = existingGlobal.length > 0 ? (existingGlobal[0].value || {}) : {};
    const globalHasDiff = !badgeValuesEqual(globalBefore, globalBadgeValue);

    if (!dryRun && globalHasDiff) {
        if (existingGlobal.length > 0) {
            await getObject('steedos_keyvalues').update(existingGlobal[0]._id, { value: globalBadgeValue });
        } else {
            await getObject('steedos_keyvalues').insert({
                user: userId,
                space: null,
                key: 'badge',
                value: globalBadgeValue
            });
        }
    }

    const globalRecord = {
        userId,
        space: null,
        before: globalBefore,
        after: globalBadgeValue,
        hasDiff: globalHasDiff
    };

    if (includeNames) {
        globalRecord.spaceName = '全局';
    }

    results.push(globalRecord);

    // Resolve names if requested
    if (includeNames) {
        try {
            const usersCol = await getCollection('users');
            const userDoc = await usersCol.findOne({ _id: userId }, { fields: { name: 1 } });
            const userName = userDoc ? (userDoc.name || userId) : userId;

            const spaceIds = results.map(r => r.space).filter(Boolean);
            let spaceMap = {};
            if (spaceIds.length > 0) {
                const spacesCol = await getCollection('spaces');
                const spaceDocs = await spacesCol.find(
                    { _id: { $in: spaceIds } },
                    { fields: { name: 1 } }
                ).toArray();
                for (const s of spaceDocs) {
                    spaceMap[s._id] = s.name || s._id;
                }
            }

            for (const r of results) {
                r.userName = userName;
                if (r.space) {
                    r.spaceName = spaceMap[r.space] || r.space;
                }
            }
        } catch (e) {
            // If name lookup fails, leave IDs as-is
            console.error('badgeRecalcExecute: failed to resolve user/space names:', e.message);
        }
    }

    return results;
}

module.exports = {
    rest: {
        method: 'GET',
        fullPath: '/api/workflow/badge-recalc-execute'
    },
    async handler(ctx) {
        const startTime = Date.now();

        // Permission check
        const userSession = ctx.meta.user;
        if (!userSession || !userSession.is_space_admin) {
            ctx.meta.$statusCode = 403;
            return { success: false, error: 'Forbidden: Admin access required' };
        }

        const {
            scope,
            userId,
            page: pageParam,
            pageSize: pageSizeParam,
            dryRun: dryRunParam
        } = ctx.params;

        // Validate scope
        if (!scope || !['user', 'all'].includes(scope)) {
            ctx.meta.$statusCode = 400;
            return { success: false, error: 'Invalid scope. Must be one of: user, all' };
        }

        if (scope === 'user' && !userId) {
            ctx.meta.$statusCode = 400;
            return { success: false, error: 'userId is required when scope=user' };
        }

        // Parse parameters
        const page = Math.max(1, parseInt(pageParam) || 1);
        const pageSize = Math.min(50, Math.max(1, parseInt(pageSizeParam) || 10));
        // dryRun defaults to true — only compute, don't write
        const dryRun = dryRunParam === 'false' || dryRunParam === false ? false : true;

        const errors = [];
        const allResults = [];

        try {
            const [categoriesCol, spaceUsersCol, keyvaluesCol] = await Promise.all([
                getCollection('categories'),
                getCollection('space_users'),
                getCollection('steedos_keyvalues')
            ]);

            let userIds = [];
            let totalUsers = 0;

            if (scope === 'user') {
                // Single user — no pagination needed
                userIds = [userId];
                totalUsers = 1;
            } else if (scope === 'all') {
                // All users with badge records, deduplicated
                const pipeline = [
                    { $match: { key: 'badge' } },
                    { $group: { _id: '$user' } },
                    { $sort: { _id: 1 } }
                ];
                const allUsers = await keyvaluesCol.aggregate(pipeline).toArray();
                totalUsers = allUsers.length;
                const start = (page - 1) * pageSize;
                userIds = allUsers.slice(start, start + pageSize).map(u => u._id);
            }

            const totalPages = scope === 'user' ? 1 : Math.ceil(totalUsers / pageSize);

            // Process each user
            for (const uid of userIds) {
                try {
                    const userResults = await recalcUserBadge(uid, dryRun, categoriesCol, spaceUsersCol, { includeNames: scope === 'user' });
                    allResults.push(...userResults);
                } catch (err) {
                    errors.push({ userId: uid, error: err.message });
                }
            }

            const elapsed = Math.round((Date.now() - startTime) / 1000) + 's';

            return {
                success: true,
                scope,
                page: scope === 'user' ? 1 : page,
                pageSize: scope === 'user' ? 1 : pageSize,
                totalUsers,
                totalPages,
                processedUsers: userIds.length,
                dryRun,
                elapsed,
                results: allResults,
                errors
            };
        } catch (err) {
            ctx.meta.$statusCode = 500;
            return {
                success: false,
                error: err.message,
                elapsed: Math.round((Date.now() - startTime) / 1000) + 's'
            };
        }
    }
};
