/**
 * Skills download — fetches skill folders from the storage API and writes them locally.
 *
 * Downloads can create or overwrite a local skill folder after approval so the
 * local copy matches the cloud version.
 */
import { createHash } from 'crypto';
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
import { dirname, join, relative } from 'path';
import { getAgentDir } from '../../config.js';
import { log } from '../../shared/logging/agent-log.js';
import { findDuplicateTeamSkills, formatDuplicateTeamSkillError, groupRemoteFilesBySkill, listAuthorizedTeamSkillFiles, listSkillFiles, storageOwnerOptionsForRemoteSkillFile } from './remote-skill-files.js';
import { downloadFile } from './storage-client.js';
const TEAM_SKILL_ACCESS_UNRESOLVED_DOWNLOAD_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 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),
            modifiedAt: statSync(fullPath).mtime
        });
    }
    return files.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
}
function maxIsoTimestamp(values) {
    let max = 0;
    for (const value of values) {
        if (!value)
            continue;
        const time = value instanceof Date ? value.getTime() : Date.parse(value);
        if (Number.isFinite(time) && time > max) {
            max = time;
        }
    }
    return max > 0 ? new Date(max).toISOString() : undefined;
}
function buildLocalSkillManifest(skillName, skillsDir) {
    const dir = join(skillsDir, skillName);
    const files = new Map();
    const hashes = new Map();
    const modifiedTimes = [];
    for (const file of collectLocalFiles(dir)) {
        files.set(file.relativePath, file.content);
        hashes.set(file.relativePath, sha256(file.content));
        modifiedTimes.push(file.modifiedAt);
    }
    return { name: skillName, dir, files, hashes, modifiedAt: maxIsoTimestamp(modifiedTimes) };
}
async function buildRemoteSkillManifest(accessToken, remoteFiles) {
    const files = new Map();
    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));
        files.set(relativePath, content);
        hashes.set(relativePath, sha256(content));
    }));
    return {
        files,
        hashes,
        source,
        modifiedAt: maxIsoTimestamp(remoteFiles.map((file) => file.entry.updated))
    };
}
function diffSkillManifests(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 buildFileDiffs(diff) {
    return [
        ...diff.modifiedFiles.map((path) => ({ path, change: 'modified' })),
        ...diff.localOnlyFiles.map((path) => ({ path, change: 'local-only' })),
        ...diff.remoteOnlyFiles.map((path) => ({ path, change: 'remote-only' }))
    ].sort((a, b) => {
        if (a.path !== b.path)
            return a.path < b.path ? -1 : 1;
        return a.change < b.change ? -1 : a.change > b.change ? 1 : 0;
    });
}
function writeManifestFiles(skillDir, remoteManifest, relativePaths) {
    let written = 0;
    for (const relativePath of relativePaths) {
        const content = remoteManifest.files.get(relativePath);
        if (!content) {
            throw new Error(`Missing remote file content for ${relativePath}`);
        }
        const localPath = join(skillDir, relativePath);
        mkdirSync(dirname(localPath), { recursive: true });
        writeFileSync(localPath, content);
        written++;
    }
    return written;
}
function overwriteLocalSkill(skillDir, remoteManifest) {
    rmSync(skillDir, { recursive: true, force: true });
    mkdirSync(skillDir, { recursive: true });
    return writeManifestFiles(skillDir, remoteManifest, remoteManifest.files.keys());
}
async function syncRemoteSkill(accessToken, skillName, remoteFiles, skillsDir, result, approve) {
    const localManifest = buildLocalSkillManifest(skillName, skillsDir);
    const remoteManifest = await buildRemoteSkillManifest(accessToken, remoteFiles);
    const diff = diffSkillManifests(localManifest, remoteManifest);
    if (localManifest.files.size === 0) {
        const fileDiffs = buildFileDiffs({
            modifiedFiles: [],
            localOnlyFiles: [],
            remoteOnlyFiles: [...remoteManifest.files.keys()].sort()
        });
        const approved = await approve({
            skillName,
            source: remoteManifest.source,
            kind: 'create',
            modifiedFiles: 0,
            localOnlyFiles: 0,
            remoteOnlyFiles: remoteManifest.files.size,
            localModifiedAt: localManifest.modifiedAt,
            cloudModifiedAt: remoteManifest.modifiedAt,
            fileDiffs
        });
        if (!approved) {
            result.skippedByUser.push(skillName);
            log.info('skills-download', `skill=${skillName} source=${remoteManifest.source} action=skip-user-new`);
            return;
        }
        const skillDir = join(skillsDir, skillName);
        result.filesDownloaded += writeManifestFiles(skillDir, remoteManifest, remoteManifest.files.keys());
        result.downloaded.push(skillName);
        result.changes.push({
            skillName,
            source: remoteManifest.source,
            kind: 'create',
            localModifiedAt: localManifest.modifiedAt,
            cloudModifiedAt: remoteManifest.modifiedAt,
            fileDiffs
        });
        log.info('skills-download', `skill=${skillName} source=${remoteManifest.source} action=download-new files=${remoteManifest.files.size}`);
        return;
    }
    if (!hasAnyDiff(diff)) {
        result.skipped.push(skillName);
        log.info('skills-download', `skill=${skillName} source=${remoteManifest.source} action=skip-unchanged`);
        return;
    }
    log.info('skills-download', `skill=${skillName} source=${remoteManifest.source} modified=${diff.modifiedFiles.length} localOnly=${diff.localOnlyFiles.length} remoteOnly=${diff.remoteOnlyFiles.length}`);
    const approved = await approve({
        skillName,
        source: remoteManifest.source,
        kind: 'update',
        modifiedFiles: diff.modifiedFiles.length,
        localOnlyFiles: diff.localOnlyFiles.length,
        remoteOnlyFiles: diff.remoteOnlyFiles.length,
        localModifiedAt: localManifest.modifiedAt,
        cloudModifiedAt: remoteManifest.modifiedAt,
        fileDiffs: buildFileDiffs(diff)
    });
    if (!approved) {
        result.skippedByUser.push(skillName);
        log.info('skills-download', `skill=${skillName} source=${remoteManifest.source} action=skip-user`);
        return;
    }
    result.filesDownloaded += overwriteLocalSkill(localManifest.dir, remoteManifest);
    result.downloaded.push(skillName);
    result.changes.push({
        skillName,
        source: remoteManifest.source,
        kind: 'update',
        localModifiedAt: localManifest.modifiedAt,
        cloudModifiedAt: remoteManifest.modifiedAt,
        fileDiffs: buildFileDiffs(diff)
    });
    log.info('skills-download', `skill=${skillName} source=${remoteManifest.source} action=overwrite files=${remoteManifest.files.size}`);
}
/**
 * Download all user and team skills from cloud storage to the local filesystem.
 */
async function syncSkillsFromCloud(accessToken, approve, authorizedSkillTeamIds) {
    const result = {
        downloaded: [],
        skipped: [],
        skippedByUser: [],
        filesDownloaded: 0,
        changes: [],
        errors: []
    };
    let userFiles = [];
    try {
        userFiles = await listSkillFiles(accessToken, 'user');
    }
    catch (err) {
        result.errors.push(`User skills: ${err instanceof Error ? err.message : String(err)}`);
    }
    let teamFiles = [];
    if (authorizedSkillTeamIds === null) {
        result.errors.push(TEAM_SKILL_ACCESS_UNRESOLVED_DOWNLOAD_WARNING);
    }
    else {
        const teamFilesResult = await listAuthorizedTeamSkillFiles(accessToken, authorizedSkillTeamIds);
        result.errors.push(...teamFilesResult.errors);
        teamFiles = teamFilesResult.files;
    }
    if (userFiles.length === 0 && teamFiles.length === 0 && result.errors.length === 0) {
        log.info('skills-download', 'No skills found in cloud storage');
        return result;
    }
    const skillsDir = getSkillsDir();
    mkdirSync(skillsDir, { recursive: true });
    const userBySkill = groupRemoteFilesBySkill(userFiles);
    const teamBySkill = groupRemoteFilesBySkill(teamFiles);
    const duplicateTeamSkills = findDuplicateTeamSkills(teamBySkill);
    const duplicateTeamSkillNames = new Set(duplicateTeamSkills.map((duplicate) => duplicate.skillName));
    result.errors.push(...duplicateTeamSkills.map(formatDuplicateTeamSkillError));
    const remoteSkillsByName = new Map(userBySkill);
    for (const duplicate of duplicateTeamSkills) {
        remoteSkillsByName.delete(duplicate.skillName);
    }
    for (const [skillName, files] of teamBySkill) {
        if (duplicateTeamSkillNames.has(skillName))
            continue;
        remoteSkillsByName.set(skillName, files);
    }
    for (const skillName of [...remoteSkillsByName.keys()].sort()) {
        const remoteFiles = remoteSkillsByName.get(skillName);
        if (!remoteFiles)
            continue;
        try {
            await syncRemoteSkill(accessToken, skillName, remoteFiles, skillsDir, result, approve);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            result.errors.push(`${skillName}: ${message}`);
        }
    }
    result.downloaded.sort();
    result.skipped.sort();
    result.skippedByUser.sort();
    log.info('skills-download', `Downloaded: ${result.downloaded.length}, skipped: ${result.skipped.length}, skippedByUser: ${result.skippedByUser.length}`);
    return result;
}
export async function downloadSkillsWithApproval(accessToken, approve, authorizedSkillTeamIds = []) {
    return syncSkillsFromCloud(accessToken, approve, authorizedSkillTeamIds);
}
//# sourceMappingURL=skills-download.js.map