/**
 * Read-only skills sync status for the interactive startup hint.
 *
 * The checker compares local skill folders against user and team cloud storage
 * without writing either side. It mirrors the safety boundary used by
 * /upload-skills: team-owned skill names are not uploadable, even when a user
 * skill with the same name also exists. Startup can therefore warn about drift
 * without promising that every local change can be pushed.
 */
import { createHash } from 'crypto';
import { existsSync, readdirSync, readFileSync } from 'fs';
import { join, relative } from 'path';
import { getAgentDir } from '../../config.js';
import { findDuplicateTeamSkills, formatDuplicateTeamSkillError, groupRemoteFilesBySkill, listAuthorizedTeamSkillFiles, listSkillFiles, storageOwnerOptionsForRemoteSkillFile } from './remote-skill-files.js';
import { downloadFile } from './storage-client.js';
const TEAM_SKILL_ACCESS_UNRESOLVED_STATUS_WARNING = 'Could not determine authorized team skill access; team skills skipped.';
function sha256(data) {
    return createHash('sha256').update(data).digest('hex');
}
function getSkillsDir() {
    return join(getAgentDir(), 'skills');
}
function listLocalSkillNames(skillsDir) {
    if (!existsSync(skillsDir))
        return [];
    return readdirSync(skillsDir, { withFileTypes: true })
        .filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
        .map((entry) => entry.name)
        .sort();
}
function collectLocalFiles(dir, rootDir = dir) {
    if (!existsSync(dir))
        return [];
    const files = [];
    for (const entry of readdirSync(dir, { withFileTypes: true })) {
        const fullPath = join(dir, entry.name);
        if (entry.isDirectory()) {
            files.push(...collectLocalFiles(fullPath, rootDir));
            continue;
        }
        if (!entry.isFile())
            continue;
        files.push({
            relativePath: relative(rootDir, fullPath).split('\\').join('/'),
            content: readFileSync(fullPath)
        });
    }
    return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
}
function buildLocalManifest(skillName, skillsDir) {
    const hashes = new Map();
    for (const file of collectLocalFiles(join(skillsDir, skillName))) {
        hashes.set(file.relativePath, sha256(file.content));
    }
    return { hashes };
}
function remoteFilesCacheKey(skillName, files) {
    const storageContext = files[0]?.storageContext ?? 'unknown';
    const ownerTeamIds = Array.from(new Set(files.map((file) => file.ownerTeamId).filter((teamId) => !!teamId))).sort();
    return `${storageContext}:${ownerTeamIds.join(',')}:${skillName}`;
}
async function buildRemoteManifest(accessToken, remoteFiles) {
    const hashes = new Map();
    const source = remoteFiles[0]?.storageContext;
    if (!source) {
        throw new Error('Remote skill manifest is empty');
    }
    const sources = new Set(remoteFiles.map((file) => file.storageContext));
    if (sources.size > 1) {
        throw new Error('skill exists in multiple cloud scopes');
    }
    await Promise.all(remoteFiles.map(async (file) => {
        const relativePath = file.entry.path.split('/').slice(1).join('/');
        const content = await downloadFile(accessToken, file.storagePath, file.storageContext, storageOwnerOptionsForRemoteSkillFile(file));
        hashes.set(relativePath, sha256(content));
    }));
    return { hashes, source };
}
function diffManifests(localManifest, remoteManifest) {
    const modifiedFiles = [];
    const localOnlyFiles = [];
    const remoteOnlyFiles = [];
    for (const [relativePath, localHash] of localManifest.hashes) {
        const remoteHash = remoteManifest.hashes.get(relativePath);
        if (!remoteHash) {
            localOnlyFiles.push(relativePath);
            continue;
        }
        if (remoteHash !== localHash) {
            modifiedFiles.push(relativePath);
        }
    }
    for (const relativePath of remoteManifest.hashes.keys()) {
        if (!localManifest.hashes.has(relativePath)) {
            remoteOnlyFiles.push(relativePath);
        }
    }
    return {
        modifiedFiles: modifiedFiles.sort(),
        localOnlyFiles: localOnlyFiles.sort(),
        remoteOnlyFiles: remoteOnlyFiles.sort()
    };
}
function hasAnyDiff(diff) {
    return (diff.modifiedFiles.length > 0 ||
        diff.localOnlyFiles.length > 0 ||
        diff.remoteOnlyFiles.length > 0);
}
function statusItem(skillName, kind, diff, source) {
    return {
        skillName,
        kind,
        modifiedFiles: diff.modifiedFiles.length,
        localOnlyFiles: diff.localOnlyFiles.length,
        remoteOnlyFiles: diff.remoteOnlyFiles.length,
        ...(source ? { source } : {})
    };
}
async function listRemoteSkills(accessToken, result, authorizedSkillTeamIds) {
    let userFiles = [];
    try {
        userFiles = await listSkillFiles(accessToken, 'user');
    }
    catch (error) {
        result.errors.push(`User skills: ${error instanceof Error ? error.message : String(error)}`);
    }
    let teamFiles = [];
    if (authorizedSkillTeamIds === null) {
        result.errors.push(TEAM_SKILL_ACCESS_UNRESOLVED_STATUS_WARNING);
    }
    else {
        const teamFilesResult = await listAuthorizedTeamSkillFiles(accessToken, authorizedSkillTeamIds);
        result.errors.push(...teamFilesResult.errors);
        teamFiles = teamFilesResult.files;
    }
    return {
        userBySkill: groupRemoteFilesBySkill(userFiles),
        teamBySkill: groupRemoteFilesBySkill(teamFiles)
    };
}
/**
 * Return local/cloud drift for skills without mutating either side.
 *
 * Classification rules:
 * - User-only cloud skills can need download, and local user skills can need upload.
 * - Team cloud skills can need download when missing locally.
 * - Existing local copies of team skills go to changedTeam when they differ, because
 *   /upload-skills refuses to upload any skill name owned by the team scope.
 * - If user and team cloud scopes both contain the same skill name, team ownership wins.
 */
export async function checkSkillsSyncStatus(accessToken, authorizedSkillTeamIds = []) {
    const result = {
        needsDownload: [],
        needsUpload: [],
        changedTeam: [],
        errors: []
    };
    const skillsDir = getSkillsDir();
    const localSkillNames = listLocalSkillNames(skillsDir);
    const { userBySkill, teamBySkill } = await listRemoteSkills(accessToken, result, authorizedSkillTeamIds);
    const duplicateTeamSkills = findDuplicateTeamSkills(teamBySkill);
    const duplicateTeamSkillNames = new Set(duplicateTeamSkills.map((duplicate) => duplicate.skillName));
    result.errors.push(...duplicateTeamSkills.map(formatDuplicateTeamSkillError));
    // Remote files are downloaded to hash them. Cache manifests within one check
    // so a user-scope diff and local upload diff do not download the same files twice.
    const remoteManifestCache = new Map();
    async function getRemoteManifest(skillName, files) {
        const cacheKey = remoteFilesCacheKey(skillName, files);
        const cached = remoteManifestCache.get(cacheKey);
        if (cached)
            return cached;
        const manifest = await buildRemoteManifest(accessToken, files);
        remoteManifestCache.set(cacheKey, manifest);
        return manifest;
    }
    // User cloud skills are uploadable/downloadable only when the team scope does
    // not own the same name. This matches /upload-skills blocking behavior.
    for (const skillName of [...userBySkill.keys()].sort()) {
        if (teamBySkill.has(skillName)) {
            continue;
        }
        const remoteFiles = userBySkill.get(skillName);
        if (!remoteFiles)
            continue;
        try {
            const localManifest = buildLocalManifest(skillName, skillsDir);
            const remoteManifest = await getRemoteManifest(skillName, remoteFiles);
            const diff = diffManifests(localManifest, remoteManifest);
            if (localManifest.hashes.size === 0) {
                result.needsDownload.push(statusItem(skillName, 'create', {
                    modifiedFiles: [],
                    localOnlyFiles: [],
                    remoteOnlyFiles: [...remoteManifest.hashes.keys()]
                }, remoteManifest.source));
            }
            else if (hasAnyDiff(diff)) {
                result.needsDownload.push(statusItem(skillName, 'update', diff, remoteManifest.source));
            }
        }
        catch (error) {
            result.errors.push(`${skillName}: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    // Team skills are download sources, but local edits are advisory only. They
    // are reported separately so startup does not imply /upload-skills will push them.
    for (const skillName of [...teamBySkill.keys()].sort()) {
        if (duplicateTeamSkillNames.has(skillName)) {
            continue;
        }
        const remoteFiles = teamBySkill.get(skillName);
        if (!remoteFiles)
            continue;
        try {
            const localManifest = buildLocalManifest(skillName, skillsDir);
            const remoteManifest = await getRemoteManifest(skillName, remoteFiles);
            const diff = diffManifests(localManifest, remoteManifest);
            if (localManifest.hashes.size === 0) {
                result.needsDownload.push(statusItem(skillName, 'create', {
                    modifiedFiles: [],
                    localOnlyFiles: [],
                    remoteOnlyFiles: [...remoteManifest.hashes.keys()]
                }, remoteManifest.source));
            }
            else if (hasAnyDiff(diff)) {
                result.changedTeam.push(statusItem(skillName, 'update', diff, remoteManifest.source));
            }
        }
        catch (error) {
            result.errors.push(`${skillName}: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    // Local skills with team-owned names are intentionally skipped here: the team
    // loop above already reported missing/different team copies, and upload is blocked.
    for (const skillName of localSkillNames) {
        if (teamBySkill.has(skillName)) {
            continue;
        }
        try {
            const localManifest = buildLocalManifest(skillName, skillsDir);
            const remoteFiles = userBySkill.get(skillName);
            if (!remoteFiles) {
                result.needsUpload.push(statusItem(skillName, 'create', {
                    modifiedFiles: [],
                    localOnlyFiles: [...localManifest.hashes.keys()],
                    remoteOnlyFiles: []
                }));
                continue;
            }
            const remoteManifest = await getRemoteManifest(skillName, remoteFiles);
            const diff = diffManifests(localManifest, remoteManifest);
            if (hasAnyDiff(diff)) {
                result.needsUpload.push(statusItem(skillName, 'update', diff));
            }
        }
        catch (error) {
            result.errors.push(`${skillName}: ${error instanceof Error ? error.message : String(error)}`);
        }
    }
    result.needsDownload.sort((a, b) => a.skillName.localeCompare(b.skillName));
    result.needsUpload.sort((a, b) => a.skillName.localeCompare(b.skillName));
    result.changedTeam.sort((a, b) => a.skillName.localeCompare(b.skillName));
    return result;
}
//# sourceMappingURL=skills-status.js.map