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

/**
 * Loads markdown content from external skill folders/files mapped to an agent.
 *
 * Unlike `knowledgeFolders` (which feed the RAG retriever and are search-gated),
 * `skillFolders` entries are loaded eagerly and concatenated into the agent's
 * system prompt — same role as the agent's own `.md` file, just sourced from
 * the user's own library outside `.agent/skills/`.
 *
 * Each spec can be:
 *   - an absolute folder path → every `.md` file directly inside is loaded
 *   - an absolute file path ending in `.md` → that single file is loaded
 *   - `~`-prefixed forms of either of the above
 *
 * We intentionally do NOT recurse into subfolders. Users who want hierarchy
 * pick the specific subfolder; that keeps the contract predictable and avoids
 * accidentally pulling in large trees.
 *
 * Skills outside the brain root are allowed (unlike knowledgeFolders) because
 * skill libraries typically live with the user (e.g. ~/Documents/agent-skills),
 * not inside a particular brain. Read-only, never written.
 */

const MAX_SKILL_FILES = 64;
const MAX_SKILL_BYTES = 512 * 1024; // 512 KB per file ceiling

export interface LoadedSkill {
    /** Display name derived from the filename (no extension). */
    name: string;
    /** Absolute path the content was read from. */
    filePath: string;
    /** Raw markdown body. */
    content: string;
}

export interface LoadedSkillBundle {
    skills: LoadedSkill[];
    /** Specs that couldn't be resolved or contained no .md files. For UI hints. */
    skipped: { spec: string; reason: string }[];
}

export function loadExternalSkills(specs: string[] | undefined): LoadedSkillBundle {
    const skills: LoadedSkill[] = [];
    const skipped: { spec: string; reason: string }[] = [];
    if (!Array.isArray(specs) || specs.length === 0) return { skills, skipped };

    const seen = new Set<string>();

    for (const rawSpec of specs) {
        if (skills.length >= MAX_SKILL_FILES) {
            skipped.push({ spec: rawSpec, reason: `최대 스킬 파일 수(${MAX_SKILL_FILES}) 초과` });
            continue;
        }
        const resolved = resolvePathInput(rawSpec || '');
        if (!resolved) {
            skipped.push({ spec: rawSpec, reason: '경로를 해석할 수 없음 (절대경로 또는 ~ 형식이어야 합니다)' });
            continue;
        }

        let stat: fs.Stats;
        try {
            stat = fs.statSync(resolved);
        } catch {
            skipped.push({ spec: rawSpec, reason: '경로가 존재하지 않음' });
            continue;
        }

        if (stat.isFile()) {
            if (!/\.(md|markdown)$/i.test(resolved)) {
                skipped.push({ spec: rawSpec, reason: '.md 파일이 아님' });
                continue;
            }
            _tryAddFile(resolved, skills, skipped, seen, rawSpec);
            continue;
        }

        if (stat.isDirectory()) {
            let entries: string[] = [];
            try {
                entries = fs.readdirSync(resolved);
            } catch (e: any) {
                skipped.push({ spec: rawSpec, reason: `폴더 읽기 실패: ${e?.message ?? e}` });
                continue;
            }
            const mdFiles = entries
                .filter((n) => /\.(md|markdown)$/i.test(n))
                .map((n) => path.join(resolved, n))
                .sort();
            if (mdFiles.length === 0) {
                skipped.push({ spec: rawSpec, reason: '폴더에 .md 파일이 없음' });
                continue;
            }
            for (const filePath of mdFiles) {
                if (skills.length >= MAX_SKILL_FILES) break;
                _tryAddFile(filePath, skills, skipped, seen, rawSpec);
            }
            continue;
        }

        skipped.push({ spec: rawSpec, reason: '파일도 폴더도 아님 (심볼릭 링크?)' });
    }

    if (skills.length > 0) {
        logInfo('external-skills: loaded.', { count: skills.length, skipped: skipped.length });
    }
    return { skills, skipped };
}

function _tryAddFile(
    absPath: string,
    skills: LoadedSkill[],
    skipped: { spec: string; reason: string }[],
    seen: Set<string>,
    originalSpec: string
): void {
    const key = path.normalize(absPath);
    if (seen.has(key)) return;
    seen.add(key);

    let content = '';
    try {
        const fileStat = fs.statSync(absPath);
        if (fileStat.size > MAX_SKILL_BYTES) {
            skipped.push({
                spec: originalSpec,
                reason: `${path.basename(absPath)}: 파일이 너무 큼 (${fileStat.size} bytes > ${MAX_SKILL_BYTES})`,
            });
            return;
        }
        content = fs.readFileSync(absPath, 'utf8');
    } catch (e: any) {
        skipped.push({ spec: originalSpec, reason: `${path.basename(absPath)} 읽기 실패: ${e?.message ?? e}` });
        logError('external-skills: file read failed.', { absPath, error: e?.message ?? String(e) });
        return;
    }

    const name = path.basename(absPath).replace(/\.(md|markdown)$/i, '');
    skills.push({ name, filePath: absPath, content });
}

/**
 * Format the bundle as a single markdown block ready to append to the agent's
 * system prompt. Returns empty string when no skills loaded — caller can then
 * skip injection entirely without an empty section header.
 */
export function formatSkillsAsPromptBlock(bundle: LoadedSkillBundle): string {
    if (!bundle.skills.length) return '';
    const parts: string[] = ['## External Skills', ''];
    for (const skill of bundle.skills) {
        parts.push(`### Skill: ${skill.name}`);
        parts.push(skill.content.trim());
        parts.push('');
    }
    return parts.join('\n').trim();
}
