import * as fs from 'fs';
import * as path from 'path';
import { isInside } from '../lib/paths';
import { logError, logInfo } from '../utils';

/**
 * External-tool skill injection.
 *
 * Connect_origin's `/api/skill-inject` writes Python tool scripts into a
 * per-agent `tools/` folder, on the assumption the agent can `<run_command>`
 * them. ConnectAI doesn't run Python tools — its agent skills are markdown
 * documents loaded by the sidebar (`<workspace>/.agent/skills/<name>.md`).
 *
 * So this service injects **markdown skills** (the ConnectAI primitive), not
 * .py scripts. The endpoint shape stays similar (name / displayName /
 * description / content / source) so the same external integrations
 * (EZER / Agent University) can target either project with a thin adapter.
 *
 * What lands on disk per inject call:
 *   .agent/skills/<safeName>.md         — markdown body
 *   .agent/skills/<safeName>.meta.json  — injectedAt + injectedFrom + display fields
 *
 * The `.md` is what the existing sidebar `_sendAgentsList` already discovers.
 * The sidecar `.meta.json` is read by future UI surfaces (provenance badges)
 * but is invisible to the legacy loader, so back-compat is preserved.
 */

export interface SkillInjectionRequest {
    /** Required. Slug-style identifier; will be sanitized to filesystem-safe form. */
    name: string;
    /** Required. The markdown body of the skill (system-prompt content). */
    content: string;
    /** Optional. Human-friendly display name. Falls back to sanitized `name`. */
    displayName?: string;
    /** Optional. One-line description shown in UI hints. */
    description?: string;
    /** Optional. External-source tag, e.g. `"ezer"` / `"agent-university"`. */
    source?: string;
}

export interface SkillInjectionResult {
    /** Sanitized name actually used on disk. */
    safeName: string;
    /** Absolute path to the written markdown file. */
    filePath: string;
    /** Absolute path to the sidecar metadata file. */
    metaPath: string;
}

export class SkillInjectionError extends Error {
    constructor(message: string, public readonly code: string) {
        super(message);
        this.name = 'SkillInjectionError';
    }
}

export interface ISkillInjectionService {
    inject(req: SkillInjectionRequest): Promise<SkillInjectionResult>;
}

export interface SkillInjectionDeps {
    /** Resolves the skills directory at the time of each call (must be absolute). */
    resolveSkillsDir: () => string;
    /** Optional fs override for unit tests; defaults to node:fs. */
    fsImpl?: Pick<typeof fs, 'existsSync' | 'mkdirSync' | 'writeFileSync'>;
    /** Optional notification hook called on successful injection. */
    onInjected?: (result: SkillInjectionResult, req: SkillInjectionRequest) => void;
}

/** Conservative skill-name sanitizer. Rejects empty / overlong / shell-unfriendly inputs. */
export function sanitizeSkillName(raw: string): string {
    if (typeof raw !== 'string') return '';
    const trimmed = raw.trim();
    if (!trimmed) return '';
    // Strip extensions if caller passed "foo.md" by mistake
    const noExt = trimmed.replace(/\.(md|markdown)$/i, '');
    // Allow only ASCII alphanumerics, `_`, `-`, `.` — block path separators,
    // shell metas, and unicode that could confuse filesystems.
    const safe = noExt.replace(/[^a-zA-Z0-9_.\-]/g, '_').replace(/^[._]+|[._]+$/g, '');
    return safe.slice(0, 80);
}

export class FileSystemSkillInjectionService implements ISkillInjectionService {
    private readonly _fs: NonNullable<SkillInjectionDeps['fsImpl']>;

    constructor(private readonly deps: SkillInjectionDeps) {
        this._fs = deps.fsImpl ?? fs;
    }

    async inject(req: SkillInjectionRequest): Promise<SkillInjectionResult> {
        const safeName = sanitizeSkillName(req.name);
        if (!safeName) {
            throw new SkillInjectionError(
                'Skill name is empty after sanitization. Use ASCII letters, digits, "_", "-", or "." only.',
                'INVALID_NAME'
            );
        }
        if (typeof req.content !== 'string' || !req.content.trim()) {
            throw new SkillInjectionError('Skill content must be a non-empty markdown string.', 'EMPTY_CONTENT');
        }

        const skillsDir = this.deps.resolveSkillsDir();
        if (!skillsDir) {
            throw new SkillInjectionError(
                'Agent skills directory is not available. Open a workspace folder first.',
                'NO_SKILLS_DIR'
            );
        }

        const targetMd = path.join(skillsDir, `${safeName}.md`);
        const targetMeta = path.join(skillsDir, `${safeName}.meta.json`);

        if (!isInside(skillsDir, targetMd) || !isInside(skillsDir, targetMeta)) {
            // Defense in depth: sanitizer should already block traversal, but the
            // path math runs again here in case skillsDir resolves to something
            // surprising (symlink, weird casing on Windows, etc.).
            throw new SkillInjectionError('Refusing to write outside the skills directory.', 'PATH_ESCAPE');
        }

        try {
            if (!this._fs.existsSync(skillsDir)) {
                this._fs.mkdirSync(skillsDir, { recursive: true });
            }
            this._fs.writeFileSync(targetMd, req.content, 'utf8');

            const meta = {
                name: safeName,
                displayName: (req.displayName || '').trim() || safeName,
                description: (req.description || '').trim() || '',
                injectedAt: new Date().toISOString(),
                injectedFrom: (req.source || '').trim() || 'external',
            };
            this._fs.writeFileSync(targetMeta, JSON.stringify(meta, null, 2), 'utf8');
        } catch (e: any) {
            const msg = e?.message ?? String(e);
            logError('Skill injection write failed.', { safeName, skillsDir, error: msg });
            throw new SkillInjectionError(`Failed to write skill files: ${msg}`, 'WRITE_FAILED');
        }

        const result: SkillInjectionResult = { safeName, filePath: targetMd, metaPath: targetMeta };
        logInfo('Skill injected.', { safeName, source: req.source, skillsDir });
        this.deps.onInjected?.(result, req);
        return result;
    }
}
