/**
 * Skill Injector Service Implementation
 *
 * Injects curated agent skills into worktrees during feature creation.
 * Handles local skill copying via fs.cp, remote skill installation via npx,
 * SKILL.md idempotency checks, and .gitignore management.
 *
 * Uses constructor-injected ExecFunction for process execution (npx, git)
 * and direct fs/promises imports for filesystem operations, following
 * the established codebase conventions.
 */
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
    var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
    if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
    else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
    return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
    if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
    return function (target, key) { decorator(target, key, paramIndex); }
};
import { access, cp, mkdir, readFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { injectable, inject } from 'tsyringe';
import { SkillSourceType } from '../../domain/generated/output.js';
const SKILLS_DIR = '.claude/skills';
const SKILL_MARKER = 'SKILL.md';
const REMOTE_SKILL_TIMEOUT_MS = 30_000;
/** Shell metacharacters that must not appear in skill source strings */
const SHELL_METACHAR_PATTERN = /[;|&$`\\(){}< >]/;
/** Patterns that indicate path traversal in skill names */
const PATH_TRAVERSAL_PATTERN = /\.\./;
let SkillInjectorService = class SkillInjectorService {
    execFile;
    constructor(execFile) {
        this.execFile = execFile;
    }
    async inject(worktreePath, config, repoRoot) {
        // Validate worktree path exists
        await access(worktreePath);
        const result = {
            injected: [],
            skipped: [],
            failed: [],
        };
        if (!config.skills.length) {
            return result;
        }
        // Bootstrap .claude/skills/ directory
        const skillsDir = join(worktreePath, SKILLS_DIR);
        await mkdir(skillsDir, { recursive: true });
        // Process each skill
        for (const skill of config.skills) {
            // Validate skill name and source
            const validationError = this.validateSkill(skill);
            if (validationError) {
                result.failed.push({ name: skill.name, error: validationError });
                continue;
            }
            // Idempotency check: skip if SKILL.md already exists
            if (await this.isSkillPresent(worktreePath, skill.name)) {
                result.skipped.push(skill.name);
                continue;
            }
            // Inject based on type
            if (skill.type === SkillSourceType.Local) {
                await this.injectLocalSkill(worktreePath, skill, repoRoot, result);
            }
            else {
                await this.injectRemoteSkill(worktreePath, skill, result);
            }
        }
        // Update .gitignore for injected and skipped untracked skills
        const allSkillNames = [...result.injected, ...result.skipped];
        await this.updateGitignore(worktreePath, allSkillNames);
        return result;
    }
    validateSkill(skill) {
        // Validate skill name
        if (PATH_TRAVERSAL_PATTERN.test(skill.name)) {
            return `invalid skill name '${skill.name}': contains path traversal`;
        }
        if (skill.name.startsWith('/')) {
            return `invalid skill name '${skill.name}': absolute path not allowed`;
        }
        if (skill.name.includes('\\')) {
            return `invalid skill name '${skill.name}': backslash not allowed`;
        }
        // Validate source for remote skills (shell injection prevention)
        if (skill.type === SkillSourceType.Remote && SHELL_METACHAR_PATTERN.test(skill.source)) {
            return `invalid source '${skill.source}': contains shell metacharacters`;
        }
        return undefined;
    }
    async isSkillPresent(worktreePath, skillName) {
        try {
            await access(join(worktreePath, SKILLS_DIR, skillName, SKILL_MARKER));
            return true;
        }
        catch {
            return false;
        }
    }
    async injectLocalSkill(worktreePath, skill, repoRoot, result) {
        const sourcePath = repoRoot ? join(repoRoot, skill.source) : skill.source;
        const destPath = join(worktreePath, SKILLS_DIR, skill.name);
        try {
            await cp(sourcePath, destPath, { recursive: true });
            result.injected.push(skill.name);
        }
        catch (error) {
            const message = error instanceof Error ? error.message : String(error);
            result.failed.push({ name: skill.name, error: message });
        }
    }
    async injectRemoteSkill(worktreePath, skill, result) {
        const controller = new AbortController();
        const timer = setTimeout(() => controller.abort(), REMOTE_SKILL_TIMEOUT_MS);
        try {
            const args = ['skills', 'add', skill.source, '--yes'];
            if (skill.remoteSkillName) {
                args.push('--skill', skill.remoteSkillName);
            }
            await this.execFile('npx', args, {
                cwd: worktreePath,
                signal: controller.signal,
            });
            result.injected.push(skill.name);
        }
        catch (error) {
            const isTimeout = error instanceof Error && error.name === 'AbortError';
            const message = isTimeout
                ? `timeout: remote skill installation exceeded ${REMOTE_SKILL_TIMEOUT_MS / 1000}s`
                : error instanceof Error
                    ? error.message
                    : String(error);
            result.failed.push({ name: skill.name, error: message });
        }
        finally {
            clearTimeout(timer);
        }
    }
    async isTrackedInGit(worktreePath, skillName) {
        try {
            await this.execFile('git', ['ls-files', '--error-unmatch', `${SKILLS_DIR}/${skillName}/`], {
                cwd: worktreePath,
            });
            return true;
        }
        catch {
            return false;
        }
    }
    async updateGitignore(worktreePath, skillNames) {
        if (!skillNames.length)
            return;
        // Determine which skills need .gitignore entries (untracked only)
        const untrackedSkills = [];
        for (const name of skillNames) {
            if (!(await this.isTrackedInGit(worktreePath, name))) {
                untrackedSkills.push(name);
            }
        }
        if (!untrackedSkills.length)
            return;
        const gitignorePath = join(worktreePath, '.gitignore');
        // Read existing .gitignore (or start with empty string)
        let existingContent = '';
        try {
            const content = await readFile(gitignorePath, 'utf-8');
            existingContent = typeof content === 'string' ? content : '';
        }
        catch {
            // File doesn't exist — will create it
        }
        // Determine which entries are new
        const existingLines = new Set(existingContent.split('\n').map((line) => line.trim()));
        const newEntries = [];
        for (const name of untrackedSkills) {
            const pattern = `${SKILLS_DIR}/${name}/`;
            if (!existingLines.has(pattern)) {
                newEntries.push(pattern);
            }
        }
        if (!newEntries.length)
            return;
        // Append new entries, ensuring trailing newline
        const needsNewline = existingContent.length > 0 && !existingContent.endsWith('\n');
        const separator = needsNewline ? '\n' : '';
        const updatedContent = `${existingContent + separator + newEntries.join('\n')}\n`;
        await writeFile(gitignorePath, updatedContent);
    }
};
SkillInjectorService = __decorate([
    injectable(),
    __param(0, inject('ExecFunction')),
    __metadata("design:paramtypes", [Function])
], SkillInjectorService);
export { SkillInjectorService };
