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

/**
 * Agent ↔ Knowledge mapping.
 *
 * MVP per the architecture proposal: each agent (markdown skill in
 * `.agent/skills/<name>.md`) is linked to one or more knowledge folders
 * inside the active Brain. The mapping is the explicit middle layer that
 * removes the "어떤 지식을 불러와야 하는가" 불확실성.
 *
 * Resolution order at load time:
 *   1. JSON file at `<workspace>/.astra/agent-knowledge-map.json`
 *      (or override path via `g1nation.skillKnowledgeMapPath`).
 *   2. VS Code setting `g1nation.skillKnowledgeMap` (fallback / shared default).
 *   3. Empty mapping — caller falls back to the whole brain.
 *
 * Folder paths inside an entry can be:
 *   - Absolute (`/Users/.../Wiki/10_Wiki/Topics`) — used verbatim.
 *   - Tilde-prefixed (`~/Wiki/10_Wiki/Topics`) — expanded.
 *   - Brain-relative (`10_Wiki/Topics`) — resolved against the active brain.
 *
 * The brain-relative form is the recommended one because it makes the same
 * map portable across machines / brains: as long as each environment's brain
 * root contains a `10_Wiki/Topics`, the mapping just works.
 */

export interface AgentKnowledgeEntry {
    /** Agent name. Matches `<name>.md` in the skills folder OR a free-form id. */
    name: string;
    /** Folders this agent should retrieve from. Absolute, ~-prefixed, or brain-relative. */
    knowledgeFolders: string[];
    /**
     * External skill folders or individual .md files mapped to this agent.
     * Each entry is either an absolute folder/file path or ~-prefixed.
     * Contents are loaded at chat time and concatenated into the agent's system
     * prompt (always-on, not search-gated like knowledgeFolders).
     */
    skillFolders?: string[];
    /** Optional: pinned model override for this agent (e.g. `qwen3:8b`). */
    model?: string;
    /**
     * Optional: per-agent Knowledge Mix override (0–100). Higher = lean harder on
     * Second Brain notes. When undefined the resolver falls back to the global
     * `g1nation.knowledgeMix.secondBrainWeight` setting. Stored as integer.
     */
    secondBrainWeight?: number;
    /** Optional: human-friendly note shown in UI hints. */
    description?: string;
}

export interface AgentKnowledgeMap {
    /** Agent name used when no explicit selection is made (e.g. Telegram default). */
    defaultAgent?: string;
    agents: AgentKnowledgeEntry[];
}

export interface ResolvedScope {
    agent: AgentKnowledgeEntry | null;
    /** Absolute folder paths constrained to live inside `brainRoot`. */
    folders: string[];
    /** Source of the mapping that produced this scope (for debug surfaces). */
    source: 'json' | 'settings' | 'none';
}

const EMPTY_MAP: AgentKnowledgeMap = { agents: [] };

const DEFAULT_JSON_RELATIVE = path.join('.astra', 'agent-knowledge-map.json');

function _safeReadJson(filePath: string): unknown | null {
    try {
        if (!fs.existsSync(filePath)) return null;
        const raw = fs.readFileSync(filePath, 'utf8');
        return JSON.parse(raw);
    } catch (e: any) {
        logError('agent-knowledge-map: JSON read failed.', { filePath, error: e?.message ?? String(e) });
        return null;
    }
}

function _coerceMap(raw: unknown): AgentKnowledgeMap {
    if (!raw || typeof raw !== 'object') return EMPTY_MAP;
    const obj = raw as Record<string, unknown>;
    const agentsRaw = Array.isArray(obj.agents) ? obj.agents : [];
    const agents: AgentKnowledgeEntry[] = [];
    for (const item of agentsRaw) {
        if (!item || typeof item !== 'object') continue;
        const a = item as Record<string, unknown>;
        const name = typeof a.name === 'string' ? a.name.trim() : '';
        if (!name) continue;
        const foldersRaw = Array.isArray(a.knowledgeFolders) ? a.knowledgeFolders : [];
        const folders = foldersRaw
            .map((f) => (typeof f === 'string' ? f.trim() : ''))
            .filter((f) => f.length > 0);
        const skillsRaw = Array.isArray(a.skillFolders) ? a.skillFolders : [];
        const skillFolders = skillsRaw
            .map((f) => (typeof f === 'string' ? f.trim() : ''))
            .filter((f) => f.length > 0);
        // Per-agent Knowledge Mix weight: only accept integers within [0, 100].
        // `null` and out-of-range numbers fall back to undefined (use global).
        let secondBrainWeight: number | undefined;
        if (typeof a.secondBrainWeight === 'number' && Number.isFinite(a.secondBrainWeight)) {
            const w = Math.round(a.secondBrainWeight);
            if (w >= 0 && w <= 100) secondBrainWeight = w;
        }
        agents.push({
            name,
            knowledgeFolders: folders,
            skillFolders: skillFolders.length > 0 ? skillFolders : undefined,
            model: typeof a.model === 'string' && a.model.trim() ? a.model.trim() : undefined,
            secondBrainWeight,
            description: typeof a.description === 'string' && a.description.trim() ? a.description.trim() : undefined,
        });
    }
    const defaultAgent = typeof obj.defaultAgent === 'string' && obj.defaultAgent.trim()
        ? obj.defaultAgent.trim()
        : undefined;
    return { defaultAgent, agents };
}

/**
 * Resolve the JSON path the user has configured (or the default convention).
 * Returns empty string when no workspace is open and no absolute override is set.
 */
export function resolveKnowledgeMapJsonPath(): string {
    const cfg = vscode.workspace.getConfiguration('g1nation');
    const override = (cfg.get<string>('skillKnowledgeMapPath', '') || '').trim();
    if (override) {
        const abs = resolvePathInput(override);
        if (abs) return abs;
    }
    const folders = vscode.workspace.workspaceFolders;
    if (folders && folders.length > 0) {
        return path.join(folders[0].uri.fsPath, DEFAULT_JSON_RELATIVE);
    }
    return '';
}

/**
 * Load the mapping. Stateless: each call re-reads disk + settings, so callers
 * always observe the latest map after `editKnowledgeMap` / settings changes.
 */
export function loadKnowledgeMap(): { map: AgentKnowledgeMap; source: ResolvedScope['source'] } {
    const jsonPath = resolveKnowledgeMapJsonPath();
    if (jsonPath) {
        const raw = _safeReadJson(jsonPath);
        if (raw) {
            return { map: _coerceMap(raw), source: 'json' };
        }
    }
    const settingsRaw = vscode.workspace.getConfiguration('g1nation').get<unknown>('skillKnowledgeMap');
    if (settingsRaw && typeof settingsRaw === 'object') {
        return { map: _coerceMap(settingsRaw), source: 'settings' };
    }
    return { map: EMPTY_MAP, source: 'none' };
}

function _normalizeAgentName(raw: string | undefined | null): string {
    if (!raw) return '';
    // Accept full filesystem paths from sidebar (`.../skills/foo.md`) and
    // collapse them to the agent name `foo`.
    const trimmed = raw.trim();
    if (!trimmed) return '';
    const base = path.basename(trimmed);
    return base.replace(/\.(md|markdown)$/i, '').trim();
}

/**
 * Resolve a single folder spec (absolute / ~-prefixed / brain-relative) to an
 * absolute path that is guaranteed to live inside `brainRoot`. Returns `null`
 * when the path can't be made safe (escapes brain root, doesn't exist, etc.).
 */
function _resolveFolderInsideBrain(spec: string, brainRoot: string): string | null {
    const trimmed = (spec || '').trim();
    if (!trimmed || !brainRoot) return null;

    let candidate = '';
    if (trimmed.startsWith('~') || path.isAbsolute(trimmed)) {
        candidate = resolvePathInput(trimmed);
    } else {
        candidate = path.normalize(path.join(brainRoot, trimmed));
    }
    if (!candidate) return null;

    // Defense in depth: even an absolute spec must resolve inside the brain
    // so the Telegram bot cannot be tricked into reading arbitrary disk via
    // a malicious mapping.
    if (!isInside(brainRoot, candidate)) {
        logError('agent-knowledge-map: folder escapes brain root, ignored.', {
            spec, candidate, brainRoot,
        });
        return null;
    }
    return candidate;
}

/**
 * Resolve which folders the named agent should retrieve from, constrained to
 * the active brain. Caller passes `brainRoot` (already resolved) so this stays
 * a pure function of inputs — easy to unit test, no VS Code coupling besides
 * the load step.
 *
 * If `agentName` is empty/unknown, falls through to `defaultAgent`. If still
 * unresolved, returns an empty folder list and the caller decides whether to
 * search the whole brain (typical chat) or refuse to answer (strict mode).
 */
export function resolveScopeForAgent(
    agentName: string | undefined | null,
    brainRoot: string
): ResolvedScope {
    const { map, source } = loadKnowledgeMap();
    const normalized = _normalizeAgentName(agentName) || (map.defaultAgent ?? '');
    const agent = normalized
        ? (map.agents.find((a) => a.name === normalized) ?? null)
        : null;
    if (!agent) {
        return { agent: null, folders: [], source };
    }
    const folders: string[] = [];
    for (const spec of agent.knowledgeFolders) {
        const resolved = _resolveFolderInsideBrain(spec, brainRoot);
        if (resolved) folders.push(resolved);
    }
    return { agent, folders, source };
}

/**
 * Convenience used by the sidebar: list every agent name in the map (for the
 * "available agents" dropdown alongside the existing skills list).
 */
export function listMappedAgents(): AgentKnowledgeEntry[] {
    return loadKnowledgeMap().map.agents;
}

/**
 * Persist the mapping to disk. Creates the `.astra` directory if needed.
 * Used by the in-sidebar mapping UI so non-developers never have to touch JSON.
 */
export function saveKnowledgeMap(map: AgentKnowledgeMap): { ok: boolean; path: string; error?: string } {
    const jsonPath = resolveKnowledgeMapJsonPath();
    if (!jsonPath) {
        return { ok: false, path: '', error: '워크스페이스가 열려있지 않습니다.' };
    }
    try {
        const dir = path.dirname(jsonPath);
        if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
        const normalized: AgentKnowledgeMap = {
            defaultAgent: map.defaultAgent?.trim() || undefined,
            agents: (map.agents || []).map((a) => {
                let secondBrainWeight: number | undefined;
                if (typeof a.secondBrainWeight === 'number' && Number.isFinite(a.secondBrainWeight)) {
                    const w = Math.round(a.secondBrainWeight);
                    if (w >= 0 && w <= 100) secondBrainWeight = w;
                }
                return {
                    name: a.name.trim(),
                    knowledgeFolders: (a.knowledgeFolders || []).map((f) => f.trim()).filter(Boolean),
                    skillFolders: (a.skillFolders || []).map((f) => f.trim()).filter(Boolean),
                    model: a.model?.trim() || undefined,
                    secondBrainWeight,
                    description: a.description?.trim() || undefined,
                };
            }).filter((a) => a.name.length > 0),
        };
        // Drop empty skillFolders arrays so the JSON stays clean for entries
        // that never used the new feature.
        for (const a of normalized.agents) {
            if (!a.skillFolders || a.skillFolders.length === 0) delete a.skillFolders;
        }
        fs.writeFileSync(jsonPath, JSON.stringify(normalized, null, 2), 'utf8');
        logInfo('agent-knowledge-map: saved.', { jsonPath, agents: normalized.agents.length });
        return { ok: true, path: jsonPath };
    } catch (e: any) {
        logError('agent-knowledge-map: save failed.', { jsonPath, error: e?.message ?? String(e) });
        return { ok: false, path: jsonPath, error: e?.message ?? String(e) };
    }
}

/**
 * Read the mapping for one agent, creating a stub entry if missing.
 * The stub is *not* written to disk — saveKnowledgeMap persists it only after
 * the user actually changes something in the UI.
 */
export function getOrCreateAgentEntry(agentName: string): AgentKnowledgeEntry {
    const normalized = _normalizeAgentName(agentName);
    const { map } = loadKnowledgeMap();
    const existing = map.agents.find((a) => a.name === normalized);
    if (existing) {
        return {
            name: existing.name,
            knowledgeFolders: [...(existing.knowledgeFolders || [])],
            skillFolders: [...(existing.skillFolders || [])],
            model: existing.model,
            secondBrainWeight: existing.secondBrainWeight,
            description: existing.description,
        };
    }
    return {
        name: normalized,
        knowledgeFolders: [],
        skillFolders: [],
    };
}

/**
 * Upsert (insert-or-update) a single agent entry and persist the map.
 * Used by the sidebar Save button.
 */
export function upsertAgentEntry(entry: AgentKnowledgeEntry): { ok: boolean; path: string; error?: string } {
    const normalizedName = _normalizeAgentName(entry.name);
    if (!normalizedName) {
        return { ok: false, path: '', error: '에이전트 이름이 비어 있습니다.' };
    }
    const { map } = loadKnowledgeMap();
    const next: AgentKnowledgeMap = {
        defaultAgent: map.defaultAgent,
        agents: [...map.agents],
    };
    const idx = next.agents.findIndex((a) => a.name === normalizedName);
    const merged: AgentKnowledgeEntry = {
        name: normalizedName,
        knowledgeFolders: entry.knowledgeFolders || [],
        skillFolders: entry.skillFolders || [],
        model: entry.model,
        secondBrainWeight: entry.secondBrainWeight,
        description: entry.description,
    };
    if (idx >= 0) next.agents[idx] = merged;
    else next.agents.push(merged);
    return saveKnowledgeMap(next);
}

/**
 * Open the JSON mapping file in the editor, scaffolding a starter document if
 * one doesn't exist yet. Idempotent — safe to wire to a `g1nation.skills.editKnowledgeMap`
 * command.
 */
export async function openKnowledgeMapEditor(): Promise<void> {
    const jsonPath = resolveKnowledgeMapJsonPath();
    if (!jsonPath) {
        vscode.window.showErrorMessage('워크스페이스가 열려있지 않거나 skillKnowledgeMapPath가 잘못되었습니다.');
        return;
    }
    try {
        if (!fs.existsSync(jsonPath)) {
            const dir = path.dirname(jsonPath);
            if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
            const starter: AgentKnowledgeMap = {
                defaultAgent: 'wiki',
                agents: [
                    {
                        name: 'wiki',
                        description: 'Second Brain (Wiki/10_Wiki/Topics) 위주 답변 에이전트',
                        knowledgeFolders: ['10_Wiki/Topics'],
                    },
                ],
            };
            fs.writeFileSync(jsonPath, JSON.stringify(starter, null, 2), 'utf8');
            logInfo('agent-knowledge-map: starter created.', { jsonPath });
        }
        const doc = await vscode.workspace.openTextDocument(jsonPath);
        // Keep the ConnectAI sidebar (column 3) untouched — open the JSON in the editor group.
        await vscode.window.showTextDocument(doc, {
            viewColumn: vscode.ViewColumn.Two,
            preview: false,
        });
    } catch (e: any) {
        logError('agent-knowledge-map: open failed.', { jsonPath, error: e?.message ?? String(e) });
        vscode.window.showErrorMessage(`매핑 파일 열기 실패: ${e?.message ?? e}`);
    }
}
