/**
 * Skills upload — pushes managed local user skills to cloud storage.
 *
 * Upload is whole-folder and git-style:
 * - new remote skill -> ask for approval, then upload
 * - existing remote skill with any diff -> ask for approval
 * - remote-only files inside an approved replaced skill are deleted
 * - remote-only skills are ignored; missing local skills never imply cloud deletion
 * - team skills are never uploaded
 */
import { createHash } from 'crypto';
import { existsSync, readdirSync, readFileSync, statSync } from 'fs';
import { join, relative } from 'path';
import { getAgentDir } from '../../config.js';
import { parseFrontmatter } from '../../shared/content/frontmatter.js';
import { log } from '../../shared/logging/agent-log.js';
import { groupRemoteFilesBySkill, listAuthorizedTeamSkillFiles, listSkillFiles, SKILLS_STORAGE_PREFIX, storageOwnerOptionsForRemoteSkillFile } from './remote-skill-files.js';
import { downloadFile, storageDelete, storageWrite, uploadFile } from './storage-client.js';
export const TEAM_SKILL_ACCESS_UNRESOLVED_UPLOAD_ERROR = 'Could not determine authorized team skill access; upload skipped to avoid overwriting a team-owned skill.';
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 validateSkillName(name, parentDirName) {
    const errors = [];
    if (name !== parentDirName) {
        errors.push(`name "${name}" does not match parent directory "${parentDirName}"`);
    }
    if (name.length > 64) {
        errors.push(`name exceeds 64 characters (${name.length})`);
    }
    if (!/^[a-z0-9-]+$/.test(name)) {
        errors.push('name contains invalid characters (must be lowercase a-z, 0-9, hyphens only)');
    }
    if (name.startsWith('-') || name.endsWith('-')) {
        errors.push('name must not start or end with a hyphen');
    }
    if (name.includes('--')) {
        errors.push('name must not contain consecutive hyphens');
    }
    return errors;
}
function validateLocalSkill(skillName, skillDir) {
    const skillPath = join(skillDir, 'SKILL.md');
    if (!existsSync(skillPath)) {
        return 'missing SKILL.md';
    }
    try {
        const rawContent = readFileSync(skillPath, 'utf-8');
        const { frontmatter } = parseFrontmatter(rawContent);
        const name = frontmatter.name || skillName;
        const nameErrors = validateSkillName(name, skillName);
        if (nameErrors.length > 0) {
            return nameErrors[0];
        }
        if (!frontmatter.description || frontmatter.description.trim() === '') {
            return 'description is required';
        }
        return null;
    }
    catch (error) {
        return error instanceof Error ? error.message : String(error);
    }
}
function collectLocalFiles(dir, rootDir = dir) {
    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();
    await Promise.all(remoteFiles.map(async (file) => {
        const relativePath = file.entry.path.split('/').slice(1).join('/');
        files.set(relativePath, file);
        const content = await downloadFile(accessToken, file.storagePath, file.storageContext, storageOwnerOptionsForRemoteSkillFile(file));
        hashes.set(relativePath, sha256(content));
    }));
    return {
        files,
        hashes,
        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;
    });
}
async function uploadLocalSkill(accessToken, localManifest) {
    await Promise.all([...localManifest.files.entries()].map(([relativePath, content]) => {
        const storagePath = `${SKILLS_STORAGE_PREFIX}${localManifest.name}/${relativePath}`;
        if (relativePath === 'SKILL.md') {
            // Manifest writes must use the API route so the backend can attach provenance metadata.
            return storageWrite(accessToken, storagePath, 'user', content, 'text/markdown; charset=utf-8');
        }
        return uploadFile(accessToken, storagePath, 'user', content);
    }));
}
async function deleteRemoteFiles(accessToken, skillName, relativePaths) {
    await Promise.all(relativePaths.map((relativePath) => storageDelete(accessToken, `${SKILLS_STORAGE_PREFIX}${skillName}/${relativePath}`, 'user')));
}
function buildUploadError(skillName, error) {
    const message = error instanceof Error ? error.message : String(error);
    return `${skillName}: ${message}`;
}
export async function uploadSkills(accessToken, approve, authorizedSkillTeamIds = null) {
    const result = {
        uploaded: [],
        skipped: [],
        skippedByUser: [],
        blockedTeam: [],
        changes: [],
        errors: []
    };
    if (authorizedSkillTeamIds === null) {
        result.errors.push(TEAM_SKILL_ACCESS_UNRESOLVED_UPLOAD_ERROR);
        return result;
    }
    const skillsDir = getSkillsDir();
    const localSkillNames = listLocalSkillNames(skillsDir);
    let remoteUserFiles = [];
    try {
        remoteUserFiles = await listSkillFiles(accessToken, 'user');
    }
    catch (error) {
        result.errors.push(`User skills: ${error instanceof Error ? error.message : String(error)}`);
        return result;
    }
    const teamFilesResult = await listAuthorizedTeamSkillFiles(accessToken, authorizedSkillTeamIds);
    result.errors.push(...teamFilesResult.errors);
    if (teamFilesResult.errors.length > 0) {
        return result;
    }
    const remoteUserBySkill = groupRemoteFilesBySkill(remoteUserFiles);
    const remoteTeamBySkill = groupRemoteFilesBySkill(teamFilesResult.files);
    for (const skillName of localSkillNames) {
        if (remoteTeamBySkill.has(skillName)) {
            result.blockedTeam.push(skillName);
            continue;
        }
        const skillDir = join(skillsDir, skillName);
        const validationError = validateLocalSkill(skillName, skillDir);
        if (validationError) {
            result.errors.push(`${skillName}: ${validationError}`);
            continue;
        }
        try {
            const localManifest = buildLocalSkillManifest(skillName, skillsDir);
            const remoteFiles = remoteUserBySkill.get(skillName);
            if (!remoteFiles) {
                const fileDiffs = buildFileDiffs({
                    modifiedFiles: [],
                    localOnlyFiles: [...localManifest.files.keys()].sort(),
                    remoteOnlyFiles: []
                });
                const approved = await approve({
                    skillName,
                    kind: 'create',
                    modifiedFiles: 0,
                    localOnlyFiles: localManifest.files.size,
                    remoteOnlyFiles: 0,
                    localModifiedAt: localManifest.modifiedAt,
                    cloudModifiedAt: undefined,
                    fileDiffs
                });
                if (!approved) {
                    result.skippedByUser.push(skillName);
                    continue;
                }
                await uploadLocalSkill(accessToken, localManifest);
                result.uploaded.push(skillName);
                result.changes.push({
                    skillName,
                    kind: 'create',
                    localModifiedAt: localManifest.modifiedAt,
                    cloudModifiedAt: undefined,
                    fileDiffs
                });
                continue;
            }
            const remoteManifest = await buildRemoteSkillManifest(accessToken, remoteFiles);
            const diff = diffSkillManifests(localManifest, remoteManifest);
            if (!hasAnyDiff(diff)) {
                result.skipped.push(skillName);
                continue;
            }
            const approved = await approve({
                skillName,
                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);
                continue;
            }
            await uploadLocalSkill(accessToken, localManifest);
            if (diff.remoteOnlyFiles.length > 0) {
                await deleteRemoteFiles(accessToken, skillName, diff.remoteOnlyFiles);
            }
            result.uploaded.push(skillName);
            result.changes.push({
                skillName,
                kind: 'update',
                localModifiedAt: localManifest.modifiedAt,
                cloudModifiedAt: remoteManifest.modifiedAt,
                fileDiffs: buildFileDiffs(diff)
            });
        }
        catch (error) {
            result.errors.push(buildUploadError(skillName, error));
        }
    }
    log.info('skills-upload', `Uploaded: ${result.uploaded.length}, skipped: ${result.skipped.length}, blocked: ${result.blockedTeam.length}`);
    return result;
}
//# sourceMappingURL=skills-upload.js.map