import * as vscode from 'vscode';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';

// ─── 브레인 프로필 인터페이스 ───
export interface BrainProfile {
    id: string;
    name: string;
    localBrainPath: string;
    secondBrainRepo?: string;
    description?: string;
}

// ─── 에이전트 설정 인터페이스 (통합 버전) ───
export interface IAgentConfig {
    ollamaUrl: string;
    defaultModel: string;
    maxTreeFiles: number;
    timeout: number;
    localBrainPath: string;
    secondBrainRepo: string;
    brainProfiles: BrainProfile[];
    activeBrainId: string;
    maxContextSize: number;
    maxAutoSteps: number;
    /** 채팅 응답 생성 temperature. 낮을수록 한국어 오타·깨진 토큰이 줄어든다. */
    chatTemperature: number;
    dryRun: boolean;
    multiAgentEnabled: boolean;
    memoryEnabled: boolean;
    memoryShortTermMessages: number;
    memoryMediumTermSessions: number;
    memoryLongTermFiles: number;
    // ─── 컨텍스트 한계 관리 ───
    contextLength: number;
    maxOutputTokens: number;
    contextSafetyMargin: number;
    contextOverflowPolicy: 'stopAtLimit' | 'truncateMiddle' | 'rollingWindow';
    autoCompactHistory: boolean;
    /** 작은 모델(≤4B) 감지 시 예산 계산에 쓸 유효 context window 상한. 0 = 비활성화. */
    smallModelContextCap: number;
    // ─── 큰 입력 Map-Reduce (긴 회의록/리서치 덤프 청킹·통합) ───
    /** 단일 사용자 입력이 창을 넘으면 청크→추출→통합으로 처리. 끄면 기존 단발 경로(잘릴 수 있음). */
    largeInputMapReduce: boolean;
    /** 단일 입력 토큰이 (유효 창 × 이 비율)을 넘으면 map-reduce 발동. 기본 0.6. */
    mapReduceTriggerRatio: number;
    /** 청크 추출 동시성. 로컬 단일 GPU 보호용으로 낮게. 기본 2. */
    mapReduceConcurrency: number;
    /** 추출 합본이 창을 넘을 때 계층적 통합 최대 깊이. 기본 3. */
    mapReduceMaxDepth: number;
    /** 최종 답변에 `[조각 k]` 출처 태그를 노출. 기본 false. */
    mapReduceShowProvenance: boolean;
    // ─── 응답 복구 (Thought Quarantine / Auto-Continuation) ───
    /** 답변이 출력 토큰 한계에 걸리면 사용자 개입 없이 내부적으로 이어서 생성. */
    autoContinueOnOutputLimit: boolean;
    /** 자동 이어쓰기 최대 횟수 (무한 반복 방지). 0 = 비활성화. */
    maxAutoContinuations: number;
    /** 모델이 내부 사고만 출력하고 답변이 없으면 "최종 답변만" 지시로 1회 재생성. */
    finalOnlyRetryOnThoughtLeak: boolean;
    // ─── Hybrid Semantic Search ───
    /**
     * Embedding model name as registered in LM Studio / Ollama. Empty disables
     * semantic search and the retriever falls back to TF-IDF only. The user
     * must load this model in the engine before enabling it here.
     */
    embeddingModel: string;
    /**
     * Blend between TF-IDF (sparse) and embedding cosine (dense) scoring.
     * 0 = TF-IDF only (status quo), 1 = embedding only.
     * Default 0.5 = equal weight, a reasonable starting point.
     */
    embeddingBlendAlpha: number;
    /**
     * Section-level chunking (Phase 1-가). true 면 brain 검색이 파일 단위가 아니라
     * `##` 헤딩 기준 *섹션 청크* 단위로 색인·스코어링한다. 긴 다주제 문서의 recall 을
     * 올린다. 기본 false (= 기존 파일 단위) — 평가 하니스로 A/B 비교 후 켜기 위함.
     */
    chunkLevelRetrieval: boolean;
    /** 섹션 청크 목표 길이(문자). 이보다 길면 문단 경계로 더 쪼갠다. */
    chunkTargetChars: number;
    /**
     * Conflict Surface — 검색된 출처의 conflictSeverity 신호를 [CONFLICT WARNINGS] 블록
     * 으로 시스템 프롬프트에 노출. v4 정책 텍스트(buildAstraModeSystemPrompt) 가 이미
     * "[CONFLICT WARNING] 플래그" 를 참조하지만, 데이터를 LLM 에 전달하지 않아 무용했음.
     * true(기본) → 충돌 감지 시 블록 주입, false → 비활성.
     */
    conflictHighlightingEnabled: boolean;
    /**
     * Conflict 자기-신호 surface 시 최소 severity 임계.
     *   'low'    → LOW 이상 (가장 민감, 노이즈 가능)
     *   'medium' → MEDIUM 이상 (기본, 균형)
     *   'high'   → HIGH 만 (가장 보수적, 강한 충돌만)
     */
    conflictSeverityThreshold: 'low' | 'medium' | 'high';
    /**
     * 교차-문서 발산 감지 (같은 주제 ≥2 chunks, 본문 Jaccard < 0.30 인 잠재 모순 쌍).
     * 자기-신호와 합쳐 [CONFLICT WARNINGS] 블록에 표시.
     */
    conflictCrossDocEnabled: boolean;
    /**
     * CoVe (Chain-of-Verification) — 답변 *작성 전* 그라운딩 체크리스트를 시스템 프롬프트에
     * 주입해 모델이 self-verify 하도록. 할루시네이션 방지 + 그라운딩 명확화.
     * true(기본) → 검색된 출처 있을 때 [VERIFICATION CHECKLIST] 블록 주입.
     */
    coveEnabled: boolean;
    /** CoVe 체크리스트에 나열할 상위 출처 개수. 기본 5. */
    coveTopSourcesCount: number;
    /**
     * CoVe Strict 모드 — 모든 사실 주장 뒤에 출처 ID([S1] 등) inline 인용 강제.
     * 답변이 좀 더 학술적·verbose 해질 수 있어 기본 off.
     */
    coveStrictMode: boolean;
    /**
     * Requirement Graph — 업무 유형(회의록/시장조사/업무조사/일정) 감지 시 필수 요소
     * 체크리스트를 시스템 프롬프트에 주입. 필수 요소 누락 방지 (신뢰 조건 T3).
     * 기본 true. (Self-Evolving OS Phase 1 / Track 2-1)
     */
    requirementGraphEnabled: boolean;
    /**
     * Requirement Coverage Check — 답변 완료 후 필수 요소 커버리지를 결정론적(정규식)으로
     * 검사, 누락 가능 요소를 footer 로 표시. LLM 호출 없음. 기본 true.
     */
    requirementCoverageEnabled: boolean;
    /**
     * Epistemic Guard — 모름/추정/확실 3분류 강제 블록. 검색 근거 없는 turn 에서
     * 단정 금지 + 원자료 역질문 우선 지시. 기본 true. (Phase 2 / Track 1-3)
     */
    epistemicGuardEnabled: boolean;
    /**
     * Confidence Engine — 답변 확신도 0~100 결정론적 산출, 업무 turn footer 표시.
     * LLM 호출 없음. 기본 true. (Phase 2 / Track 1-1)
     */
    confidenceEngineEnabled: boolean;
    /**
     * Escalation Engine — 확신도 낮음/출처 충돌/출처 누락 시 footer 로 인간 검토 요청.
     * confidenceEngine 에 종속. 기본 true. (Phase 2 / Track 1-2)
     */
    escalationEnabled: boolean;
    /**
     * Critic Loop — 결정론적 검사(커버리지 누락 또는 확신도<70)가 문제를 신호한 업무
     * turn 에만 LLM 검수 1회 실행, 발견 이슈·보완 제안을 footer 카드로 표시.
     * 깨끗한 답변에는 안 돈다 (latency 보호). 기본 true. (Phase 1 / Track 2-3)
     */
    criticLoopEnabled: boolean;
    /**
     * Reflection — 업무 turn 의 결정론적 회고(확신도·누락 요소·에스컬레이션)를
     * <brain>/.astra/growth/reflections.jsonl 에 기록. 성장 추이·반복 실수 집계의
     * 원천이며, 반복 누락 요소는 Requirement Graph 블록에 강조 표시된다.
     * 기본 true. (Phase 1 / Track 2-4 + Phase 3 / Track 3-6)
     */
    reflectionEnabled: boolean;
    /**
     * Organizational Memory — <brain>/.astra/organization.md 의 조직 규칙·업무 방식을
     * 시스템 프롬프트에 항상 주입. 파일 없으면 no-op. 기본 true. (Phase 5 / Track 5-2)
     */
    orgMemoryEnabled: boolean;
    /**
     * Actionability — "현재 작업 상태" 신호(최근 슬래시 명령 + 열린 파일) 로 검색 결과
     * 재가중. TF-IDF 매치 점수에 actionability boost 추가해 "지금 작업 중인 컨텍스트" 와
     * 직접 연결된 문서를 우선. 기본 true.
     */
    actionabilityEnabled: boolean;
    /**
     * Memory Lifecycle — Distillation Loop: stale Episodic Memory 를 LongTerm 'episode-digest'
     * 로 승급. 누적 epimemory 가 검색 노이즈가 되는 것 방지.
     * true(기본) → /memory distill + 세션 종료 시 자동 트리거 (interval 기준).
     */
    distillationEnabled: boolean;
    /** 며칠 이상 지난 episode 를 distill 대상으로. 기본 30. */
    distillationAgeThresholdDays: number;
    /** 자동 distillation 의 최소 간격 (일). 너무 자주 안 돌도록. 기본 7. */
    distillationIntervalDays: number;
    /**
     * Archive 모드:
     *   - 'mark-promoted' (기본): promoted=true 마킹만, 파일 보존
     *   - 'archive-file': promoted 마킹 + 파일을 memory/episodes/archive/ 로 이동
     */
    distillationArchiveMode: 'mark-promoted' | 'archive-file';
    /**
     * Hierarchical Context Window — 질의·문서 추상도(concrete/operational/strategic) 매칭으로
     * 검색 결과 재가중. 같은 레벨 boost (× 1.15), 양 끝 mismatch penalty (× 0.7). LLM 호출 없음.
     */
    hierarchicalReweightEnabled: boolean;
    /**
     * Semantic Re-ranking — 토큰 예산 통과한 selectedChunks 의 *순서* 를 LLM 한 번 호출로
     * 재정렬. 의도 매치를 키워드 매치보다 우선. 매 turn 1회 추가 LLM 호출 (latency 비용).
     * 기본 OFF — 명시적으로 on 해야 함.
     */
    semanticRerankEnabled: boolean;
    /** 재정렬 전용 모델 ID. 비면 defaultModel 사용. 빠른 작은 모델 권장. */
    semanticRerankModel: string;
    /** Re-rank 대상 상위 후보 개수. 기본 15. */
    semanticRerankCandidateK: number;
    /** Re-rank LLM 호출 타임아웃 (초). 기본 8. */
    semanticRerankTimeoutSec: number;
    /**
     * Intent Clarification — 모호 질의에서 *추측 답변 대신 역질문* 지시.
     * 휴리스틱 차원(환경/대상/범위/포맷/마감) 별 trigger + specifier 매치. 기본 true.
     */
    intentClarificationEnabled: boolean;
    /**
     * 모호 판정 임계:
     *   - 'low': 2개 이상 missing dimension 일 때만 ambiguous (가장 덜 묻기)
     *   - 'medium' (기본): 1개 이상 missing → ambiguous
     *   - 'high': 1개 이상 missing OR 짧은 prompt(<20자)+trigger 있으면 ambiguous (가장 자주 묻기)
     */
    intentClarificationStrictness: 'low' | 'medium' | 'high';
    /**
     * Citation Trace — 답변 끝에 "출처:" 한 줄 정리 지시. CoVe Strict 의 가벼운 형제.
     * 검색 결과 있을 때만 동작. 기본 true.
     */
    citationTraceEnabled: boolean;
    /**
     * Post-hoc Self-Check — 답변 *완료 후* LLM 1회 호출로 검증 (답변 직접도 / 그라운딩 /
     * 논리 모순). 매 turn 추가 LLM 호출 (latency 비용). 기본 OFF — 명시적 opt-in.
     * 결과는 답변 아래 footer 한 줄로 표시.
     */
    selfCheckEnabled: boolean;
    /** Self-check 전용 모델 ID. 비면 defaultModel. 빠른 작은 모델 권장. */
    selfCheckModel: string;
    /** Self-check LLM 호출 타임아웃 (초). 기본 6. */
    selfCheckTimeoutSec: number;
    /**
     * Terminology Dictionary — 사용자 편집 글로서리 파일을 시스템 프롬프트에 주입,
     * 표준 표기 강제 + 답변 직전 자기 점검(Term Check). 기본 true. 파일 없으면 자동 no-op.
     */
    glossaryEnabled: boolean;
    /** Glossary 파일 상대 경로 (workspace root 기준). 기본 '.astra/glossary.md'. */
    glossaryPath: string;
    /** Glossary 본문 시스템 프롬프트 cap (chars). 너무 크면 토큰 비용↑. 기본 4000. */
    glossaryMaxBodyLength: number;
    /**
     * Post-gen Term Validator — 답변 완료 후 글로서리 forbidden 단어 결정론적 스캔.
     * Terminology Dictionary (v2.2.192) 의 *instructional* 지시를 *deterministic* 검증으로 보완.
     * LLM 호출 없음 (정규식), 매 turn 안전 실행. footer 한 줄 표시. 기본 true.
     */
    termValidatorEnabled: boolean;
    /**
     * Global Knowledge Mix weight (0–100). Controls how much the assistant leans on
     * Second Brain evidence vs. model general knowledge when answering.
     *   0   → Second Brain disabled; model knowledge only.
     *   50  → Balanced (default).
     *   100 → Second Brain is primary evidence; model knowledge only fills gaps.
     * Per-agent overrides live in AgentKnowledgeEntry.secondBrainWeight and win.
     */
    knowledgeMixSecondBrainWeight: number;
    /**
     * [Self-Reflection] Researcher와 Writer 사이에 메타인지 단계(Reflector)를 삽입할지 여부.
     * true(기본): Reflector가 plan/research를 비판적으로 검토한 critique을 Writer에 주입.
     * false: 기존 3단계(Planner→Researcher→Writer) 그대로 — 1 LLM 호출 절약 (저성능 모델/저지연 우선 시).
     */
    /**
     * Model id used by the 1인 기업 mode intent classifier (route message to
     * pipeline vs casual chat). Empty → falls back to `defaultModel`. Recommended
     * a fast small model (gemma e2b 등) so classification adds <1 s per send.
     */
    companyIntentClassifierModel: string;
    /**
     * Bypass the intent classifier and always run the full pipeline. Legacy
     * behaviour. Off by default because chat / question / thanks shouldn't
     * dispatch all agents.
     */
    companyDisableIntentClassifier: boolean;
    /**
     * 분류기가 추천한 파이프라인으로 *이번 turn만* 자동 전환할지. 켜면
     * 사용자가 명시적으로 활성화해 둔 파이프라인보다 분류기 추천이 우선.
     * 끄면 분류기 추천은 채팅 라벨에만 표시되고 dispatch는 사용자 활성
     * 파이프라인 그대로. 처음 써 보는 사용자는 끈 채 추천만 보고 점차
     * 신뢰 생기면 켜는 흐름을 권장 — 기본 false.
     */
    companyAutoSelectPipeline: boolean;
    /**
     * Intent Alignment 모드. new_task 발생 시 사용자 의도를 C-G-C-F-Q로
     * 정리하는 단계를 어떻게 다룰지.
     *   - 'off'    : alignment 비활성. 분류기가 new_task 라고 하면 곧장 pipeline.
     *   - 'smart'  : 기본값. confidence high면 자동 진행, medium/low면 사용자 확인.
     *   - 'strict' : confidence 무관 항상 사용자에게 contract 확인 카드 띄움.
     */
    companyIntentAlignmentMode: 'off' | 'smart' | 'strict';
    /** alignment 라운드 최대 횟수 (질문→답변 사이클). 1~5. */
    companyIntentAlignmentMaxRounds: number;
    /**
     * URL 자동 수집 — 사용자 프롬프트에 http(s) URL 이 있으면 LLM 호출 전에
     * 본문을 가져와 컨텍스트로 주입 (일반 챗 + 기업 모드 + alignment 공통 게이트).
     * Bridge 추출 우선, 실패 시 직접 fetch 폴백.
     */
    webAutoFetchEnabled: boolean;
    /**
     * Alignment 자가 조사 — openQuestions 를 사용자에게 보여주기 전에 두뇌를
     * 검색해 스스로 답할 수 있는 질문을 걸러낸다. 끄면 기존 동작 (질문 즉시 노출).
     */
    companyAlignmentSelfResearch: boolean;
    /**
     * Alignment 학습 루프 — 사용자가 alignment 라운드에서 직접 답한 Q/A 를
     * 두뇌의 "Alignment Knowledge" 폴더에 노트로 저장. 다음 turn 의 자가 조사가
     * 이 노트를 발견해 같은 질문을 두 번 묻지 않게 된다.
     */
    companyAlignmentKnowledgeSave: boolean;
    /**
     * Pixel Office 시각화 패널을 사이드바에 표시할지 여부. UI layer 전용 —
     * 끈다고 Agent 행동이 바뀌지 않는다. Off면 webview는 패널을 숨기고
     * 백엔드도 broadcast 자체를 skip해서 자원 절약.
     */
    /**
     * Self-Reflector Phase A — 모든 LLM 응답 끝에 [Self-Reflector Check]
     * 자기검증 블록을 자동으로 붙이게 한다. 추가 LLM 콜 없음. 본질적으로
     * 응답 품질 안전망이라 끌 이유는 적지만, 잡담 위주 환경에서 노이즈로
     * 느껴진다면 꺼둘 수 있다.
     */
    selfReflectorEnabled: boolean;
    /**
     * Hollow Code Check — `<create_file>` 등 action-tag 로 생성된 파일이 *빈 깡통*
     * (empty class, stub-only function body, imports-only file 등) 인지 정규식으로
     * 스캔. LLM 호출 0 — 휴리스틱 only.
     *
     * 옛 구조: selfReflector Phase A 가 켜져 있어야 동작 → 다수 사용자가 안전망 부재.
     * 새 구조: 독립 toggle, *기본 ON*. action-tag 가 있는 응답에 무조건 실행. 검출 시
     * actionReport 에 "⚠️ 빈 깡통" 라인 추가 (LLM 콜 없음 — 비용 0).
     */
    hollowCheckEnabled: boolean;
    /**
     * Hollow 감지 시 1회 자동 재작업. Phase B (selfReflectorExternalEnabled) 와 분리 —
     * dev-impl 같은 stage 당 정해진 단발 추가 호출 (예측 가능). 기본 ON.
     * 재작업 결과도 hollow 한 번 더 검사 → 재차 hollow 면 사용자 경고 (무한 루프 방지).
     */
    hollowCheckAutoRetry: boolean;
    /**
     * Self-Reflector Phase B — 회사 모드 specialist 응답 직후 *분리된 콘텍스트*
     * 에서 LLM 한 번 더 호출해 외부 시각으로 검증. 실패 시 1회 retry. 비용
     * 추가되므로 기본 OFF.
     */
    selfReflectorExternalEnabled: boolean;
    /**
     * Self-Reflector Phase C — 코드 산출물에 한해 syntax/lint를 실제로 돌려
     * 실행 기반 검증. Python: py_compile, JS: node --check, TS: tsc --noEmit.
     * 실패 시 에러를 응답에 첨부. 기본 OFF — 사용자 환경에 toolchain이
     * 깔려 있어야 의미가 있다.
     */
    selfReflectorExecutionEnabled: boolean;
    companyPixelOfficeEnabled: boolean;
    /**
     * Pixel Office의 캐릭터 말풍선 연출을 켤지. enabled가 true이고 이 값도
     * true일 때만 말풍선이 생성된다. 시끄럽게 느껴지면 사용자가 끌 수 있게.
     */
    companyPixelOfficeBubbles: boolean;
    /**
     * Multi-Agent 발동 모드:
     *  - 'auto' (기본): 작은 모델(≤4B) 감지 OR prompt가 컨텍스트의 큰 비중을 차지할 때만 자동 발동.
     *  - 'always': 인사·짧은 잡담을 제외한 모든 요청에 5단계 파이프라인 사용.
     *  - 'off': 기존 single-agent 동작 (수동 토글 / 키워드 매칭만 사용).
     */
    workflowMultiAgentMode: 'auto' | 'always' | 'off';
    /**
     * 'auto' 모드에서 prompt + brain context 토큰이 contextLength 의 이 비율(0~1)을 넘으면 강제 5단계.
     * 기본 0.30 — 작은 모델이 30% 이상을 input으로 먹기 시작하면 한 번에 끝내려는 시도가 위험.
     */
    workflowAutoCtxFractionThreshold: number;
    /**
     * 절대 토큰 임계값 — 입력 prompt 가 이 값 *미만* 이면 Multi-Agent 파이프라인 발동
     * 안 함 (키워드·길이 트리거 무시). 모델이 단일 호출로 처리.
     *
     * 의도: 사용자가 "요약/리뷰" 같은 키워드만 써도 chunked 가 강제로 발동해
     * LLM 여러 번 호출되며 답변이 느려지는 문제 해결. 입력이 모델 윈도우 대비
     * 충분히 작으면 한 번에 답하는 게 합리적.
     *
     * 기본 50000 — 대부분의 사용 환경에 적합. 매우 작은 컨텍스트 모델로 큰 입력을
     * 자주 다룬다면 OOM 방지 차원에서 사용자가 직접 낮출 수 있음 (Astra Settings 패널).
     */
    chunkedSwitchTokens: number;
    /**
     * Chunked 파이프라인 진입 시 outline 이 만들 수 있는 *최대 섹션 수*.
     * 실제 LLM 호출 횟수 = 1(outline) + N(section) + 1(polish) = 2 + N.
     * 따라서 이 값이 3이면 최대 5회, 4이면 최대 6회.
     *
     * 작을수록 답변 속도 빠름, 클수록 답변이 더 세분화. 기본 3 — 사용자
     * 피드백("6회 이상은 과하다") 반영. 1~10 범위 clamp.
     */
    chunkedMaxSections: number;
    /**
     * ChunkedWriter 의 polish persona 를 사용자가 override 할 텍스트. 비어 있으면
     * 기본 polish persona (DEFAULT_POLISH_PERSONA) 사용. 입력이 있으면 그 텍스트가
     * 그대로 system prompt 로 들어가 polish 단계의 톤·구조 룰을 정의.
     *
     * 의도: 사용자가 답변 톤을 (예: 격식체·반말·법률 문서·마케팅 카피) 도메인에
     * 맞게 직접 조정 가능. 코드 변경 없이 Settings 패널 textarea 만으로.
     *
     * 빈 문자열 / 공백만이면 default 사용 — 잘못 입력해도 답변이 깨지진 않음.
     */
    polishPersonaOverride: string;
    // ─── Stream 표시 ───
    /**
     * 모델 토큰을 받는 즉시 채팅 버블에 흘려보낼지 여부.
     *  - false(기본): 토큰은 내부에서만 누적, sanitize 끝난 최종 답변만 한 번에 표시 → Harmony/think 마커 누설 원천 차단.
     *  - true: legacy 라이브 스트리밍. 모델 출력에 control token 이 섞여 나오면 잠깐 화면에 보일 수 있음.
     */
    liveStreamTokens: boolean;
    /**
     * 최종 답변 포맷.
     *  - 'plain' (기본): 모델이 무심코 내보낸 `##`, `**`, `__`, `> `, `* ` 등의 마크다운 마커를 후처리로 모두 제거.
     *    섹션 라벨 텍스트(예: "핵심 요약")는 유지되지만 헤더 마커는 사라져 깔끔한 plain text 로 표시.
     *  - 'markdown': legacy 동작. 모델 출력을 그대로 렌더러에 넘김.
     */
    outputFormat: 'plain' | 'markdown';
    /**
     * 자동 기록 (project chronicle auto-record). true 면 매 prompt 후 의미 있는 turn 을
     * Wiki/Chronicle 폴더에 자동으로 저장. false 면 자동 저장 OFF (수동 기록은 계속 가능).
     * 사이드바 도구 드롭다운의 토글 항목으로 즉시 변경 가능.
     */
    chronicleAutoRecord: boolean;
    // ─── LM Studio sampling (applied to both SDK and REST paths) ───
    /** LM Studio nucleus sampling cutoff (0~1). Lower tightens; 1 disables. */
    lmStudioTopP: number;
    /** LM Studio top-K cutoff (0 disables). */
    lmStudioTopK: number;
    /** LM Studio min-P floor (0~1, 0 disables). */
    lmStudioMinP: number;
    /** LM Studio repeat penalty (1 disables, 1.05–1.2 typical). */
    lmStudioRepeatPenalty: number;
    /** Render tok/s + TTFT from prediction stats into context-budget badge. */
    lmStudioShowStatsInBudget: boolean;
    /** LM Studio model key of a small draft model for speculative decoding ('' = disabled). */
    lmStudioDraftModel: string;
    /** Load-time options. Read once per load(); changing these after load needs a reload. */
    lmStudioLoad: {
        flashAttention: boolean;
        /** "max" | "off" | number 0-1 */
        gpuOffloadRatio: 'max' | 'off' | number;
        offloadKVCacheToGpu: boolean;
        keepModelInMemory: boolean;
        useFp16ForKVCache: boolean;
        /** 0 = engine default */
        evalBatchSize: number;
    };
}

// ─── 경로 정규화 유틸리티 ───
function normalizePath(p: string): string {
    if (!p) return p;
    if (p.startsWith('~/')) {
        return path.join(os.homedir(), p.substring(2));
    }
    return p.trim();
}

function toBrainProfile(raw: Partial<BrainProfile> | undefined, fallbackIndex: number): BrainProfile | null {
    if (!raw) return null;
    const localBrainPath = normalizePath(raw.localBrainPath || '');
    if (!localBrainPath) return null;
    return {
        id: (raw.id || `brain-${fallbackIndex + 1}`).trim(),
        name: (raw.name || path.basename(localBrainPath) || `Brain ${fallbackIndex + 1}`).trim(),
        localBrainPath,
        secondBrainRepo: (raw.secondBrainRepo || '').trim(),
        description: (raw.description || '').trim()
    };
}

// ─── VS Code 설정에서 읽어오는 값 (통합 구현) ───
export function getConfig(): IAgentConfig {
    const cfg = vscode.workspace.getConfiguration('g1nation');
    
    // 브레인 프로필 로직 (utils.ts에서 이관)
    const legacyBrainPath = cfg.get<string>('localBrainPath', '');
    const legacyBrainRepo = cfg.get<string>('secondBrainRepo', '');
    const configuredProfiles = cfg.get<Partial<BrainProfile>[]>('brainProfiles', []);
    const profiles = configuredProfiles
        .map((profile, index) => toBrainProfile(profile, index))
        .filter((profile): profile is BrainProfile => !!profile);

    if (profiles.length === 0) {
        const fallbackPath = normalizePath(legacyBrainPath) || path.join(os.homedir(), '.g1nation-brain');
        profiles.push({
            id: 'default-brain',
            name: 'Local Brain',
            localBrainPath: fallbackPath,
            secondBrainRepo: legacyBrainRepo.trim(),
            description: legacyBrainPath
                ? 'Migrated from your existing localBrainPath setting'
                : 'Auto-created local knowledge folder.'
        });
    }

    const activeBrainId = cfg.get<string>('activeBrainId', profiles[0].id) || profiles[0].id;
    const activeBrain = profiles.find((profile) => profile.id === activeBrainId) || profiles[0];

    return {
        ollamaUrl: cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434',
        defaultModel: cfg.get<string>('defaultModel', 'gemma4:e2b') || 'gemma4:e2b',
        maxTreeFiles: 200,
        timeout: cfg.get<number>('requestTimeout', 300) * 1000,
        localBrainPath: activeBrain.localBrainPath,
        secondBrainRepo: activeBrain.secondBrainRepo || '',
        brainProfiles: profiles,
        activeBrainId: activeBrain.id,
        maxContextSize: cfg.get<number>('maxContextSize', 12000),
        maxAutoSteps: cfg.get<number>('maxAutoSteps', 50),
        chatTemperature: Math.min(2, Math.max(0, cfg.get<number>('chatTemperature', 0.3))),
        dryRun: cfg.get<boolean>('dryRun', false),
        multiAgentEnabled: cfg.get<boolean>('multiAgentEnabled', false),
        memoryEnabled: cfg.get<boolean>('memoryEnabled', true),
        memoryShortTermMessages: Math.max(0, cfg.get<number>('memoryShortTermMessages', 8)),
        memoryMediumTermSessions: Math.max(0, cfg.get<number>('memoryMediumTermSessions', 5)),
        memoryLongTermFiles: Math.max(0, cfg.get<number>('memoryLongTermFiles', 6)),
        contextLength: Math.max(2048, cfg.get<number>('contextLength', 32768)),
        maxOutputTokens: Math.max(256, cfg.get<number>('maxOutputTokens', 4096)),
        contextSafetyMargin: Math.max(0, cfg.get<number>('contextSafetyMargin', 2048)),
        contextOverflowPolicy: ((): IAgentConfig['contextOverflowPolicy'] => {
            const v = cfg.get<string>('contextOverflowPolicy', 'stopAtLimit');
            return v === 'truncateMiddle' || v === 'rollingWindow' ? v : 'stopAtLimit';
        })(),
        autoCompactHistory: cfg.get<boolean>('autoCompactHistory', true),
        smallModelContextCap: Math.max(0, cfg.get<number>('smallModelContextCap', 0)),
        largeInputMapReduce: cfg.get<boolean>('largeInputMapReduce', true),
        mapReduceTriggerRatio: Math.min(0.95, Math.max(0.3, cfg.get<number>('mapReduceTriggerRatio', 0.6))),
        mapReduceConcurrency: Math.min(8, Math.max(1, cfg.get<number>('mapReduceConcurrency', 2))),
        mapReduceMaxDepth: Math.min(6, Math.max(1, cfg.get<number>('mapReduceMaxDepth', 3))),
        mapReduceShowProvenance: cfg.get<boolean>('mapReduceShowProvenance', false),
        autoContinueOnOutputLimit: cfg.get<boolean>('autoContinueOnOutputLimit', true),
        maxAutoContinuations: Math.max(0, Math.min(10, cfg.get<number>('maxAutoContinuations', 4))),
        finalOnlyRetryOnThoughtLeak: cfg.get<boolean>('finalOnlyRetryOnThoughtLeak', true),
        embeddingModel: (cfg.get<string>('embeddingModel', '') || '').trim(),
        embeddingBlendAlpha: Math.max(0, Math.min(1, cfg.get<number>('embeddingBlendAlpha', 0.5))),
        chunkLevelRetrieval: cfg.get<boolean>('chunkLevelRetrieval', true),
        chunkTargetChars: Math.max(400, Math.min(4000, cfg.get<number>('chunkTargetChars', 1200))),
        conflictHighlightingEnabled: cfg.get<boolean>('conflictHighlightingEnabled', true),
        conflictSeverityThreshold: (cfg.get<string>('conflictSeverityThreshold', 'medium') as 'low' | 'medium' | 'high') || 'medium',
        conflictCrossDocEnabled: cfg.get<boolean>('conflictCrossDocEnabled', true),
        coveEnabled: cfg.get<boolean>('coveEnabled', true),
        coveTopSourcesCount: Math.max(1, Math.min(15, cfg.get<number>('coveTopSourcesCount', 5))),
        coveStrictMode: cfg.get<boolean>('coveStrictMode', false),
        requirementGraphEnabled: cfg.get<boolean>('requirementGraphEnabled', true),
        requirementCoverageEnabled: cfg.get<boolean>('requirementCoverageEnabled', true),
        epistemicGuardEnabled: cfg.get<boolean>('epistemicGuardEnabled', true),
        confidenceEngineEnabled: cfg.get<boolean>('confidenceEngineEnabled', true),
        escalationEnabled: cfg.get<boolean>('escalationEnabled', true),
        criticLoopEnabled: cfg.get<boolean>('criticLoopEnabled', true),
        reflectionEnabled: cfg.get<boolean>('reflectionEnabled', true),
        orgMemoryEnabled: cfg.get<boolean>('orgMemoryEnabled', true),
        actionabilityEnabled: cfg.get<boolean>('actionabilityEnabled', true),
        distillationEnabled: cfg.get<boolean>('distillationEnabled', true),
        distillationAgeThresholdDays: Math.max(1, Math.min(365, cfg.get<number>('distillationAgeThresholdDays', 30))),
        distillationIntervalDays: Math.max(1, Math.min(90, cfg.get<number>('distillationIntervalDays', 7))),
        distillationArchiveMode: (cfg.get<string>('distillationArchiveMode', 'mark-promoted') as 'mark-promoted' | 'archive-file') || 'mark-promoted',
        hierarchicalReweightEnabled: cfg.get<boolean>('hierarchicalReweightEnabled', true),
        semanticRerankEnabled: cfg.get<boolean>('semanticRerankEnabled', false),
        semanticRerankModel: cfg.get<string>('semanticRerankModel', '') || '',
        semanticRerankCandidateK: Math.max(2, Math.min(30, cfg.get<number>('semanticRerankCandidateK', 15))),
        semanticRerankTimeoutSec: Math.max(1, Math.min(60, cfg.get<number>('semanticRerankTimeoutSec', 8))),
        intentClarificationEnabled: cfg.get<boolean>('intentClarificationEnabled', true),
        intentClarificationStrictness: (cfg.get<string>('intentClarificationStrictness', 'medium') as 'low' | 'medium' | 'high') || 'medium',
        citationTraceEnabled: cfg.get<boolean>('citationTraceEnabled', true),
        selfCheckEnabled: cfg.get<boolean>('selfCheckEnabled', false),
        selfCheckModel: cfg.get<string>('selfCheckModel', '') || '',
        selfCheckTimeoutSec: Math.max(1, Math.min(60, cfg.get<number>('selfCheckTimeoutSec', 6))),
        glossaryEnabled: cfg.get<boolean>('glossaryEnabled', true),
        glossaryPath: cfg.get<string>('glossaryPath', '.astra/glossary.md') || '.astra/glossary.md',
        glossaryMaxBodyLength: Math.max(500, Math.min(20000, cfg.get<number>('glossaryMaxBodyLength', 4000))),
        termValidatorEnabled: cfg.get<boolean>('termValidatorEnabled', true),
        knowledgeMixSecondBrainWeight: Math.max(0, Math.min(100, Math.round(
            cfg.get<number>('knowledgeMix.secondBrainWeight', 50)
        ))),
        companyIntentClassifierModel: (cfg.get<string>('company.intentClassifierModel', '') || '').trim(),
        companyDisableIntentClassifier: cfg.get<boolean>('company.disableIntentClassifier', false),
        companyAutoSelectPipeline: cfg.get<boolean>('company.autoSelectPipeline', true),
        companyIntentAlignmentMode: ((): 'off' | 'smart' | 'strict' => {
            const v = (cfg.get<string>('company.intentAlignmentMode', 'smart') || 'smart').trim().toLowerCase();
            return v === 'off' || v === 'strict' ? v : 'smart';
        })(),
        // ALIGNMENT_DEFAULT_MAX_ROUNDS 와 일치 — `intentAlignment` 모듈을 직접 import 하지 않은
        // 이유는 config 가 features/ 아래 모듈을 의존하면 의도치 않은 순환 import 가 생기기 때문.
        // 둘이 어긋나면 안 되므로 변경 시 양쪽 같이 갱신.
        companyIntentAlignmentMaxRounds: Math.max(1, Math.min(5, cfg.get<number>('company.intentAlignmentMaxRounds', 3))),
        webAutoFetchEnabled: cfg.get<boolean>('web.autoFetchUrls', true),
        companyAlignmentSelfResearch: cfg.get<boolean>('company.alignmentSelfResearch', true),
        companyAlignmentKnowledgeSave: cfg.get<boolean>('company.alignmentKnowledgeSave', true),
        selfReflectorEnabled: cfg.get<boolean>('selfReflector.enabled', false),
        hollowCheckEnabled: cfg.get<boolean>('hollowCheck.enabled', true),
        hollowCheckAutoRetry: cfg.get<boolean>('hollowCheck.autoRetry', true),
        selfReflectorExternalEnabled: cfg.get<boolean>('selfReflector.externalVerification', false),
        selfReflectorExecutionEnabled: cfg.get<boolean>('selfReflector.executionVerification', false),
        companyPixelOfficeEnabled: cfg.get<boolean>('company.pixelOffice.enabled', true),
        companyPixelOfficeBubbles: cfg.get<boolean>('company.pixelOffice.bubbles', true),
        workflowMultiAgentMode: ((): 'auto' | 'always' | 'off' => {
            const v = (cfg.get<string>('workflow.multiAgentMode', 'auto') || 'auto').trim().toLowerCase();
            return v === 'always' || v === 'off' ? v : 'auto';
        })(),
        workflowAutoCtxFractionThreshold: Math.max(0.05, Math.min(0.95,
            cfg.get<number>('workflow.autoCtxFractionThreshold', 0.30)
        )),
        chunkedSwitchTokens: Math.max(1000, cfg.get<number>('chunkedSwitchTokens', 50000)),
        chunkedMaxSections: Math.max(1, Math.min(10, cfg.get<number>('chunkedMaxSections', 3))),
        polishPersonaOverride: (cfg.get<string>('polishPersonaOverride', '') || '').trim(),
        liveStreamTokens: cfg.get<boolean>('liveStreamTokens', true),
        outputFormat: ((): 'plain' | 'markdown' => {
            // 기본 'markdown' — 채팅 webview 가 marked 로 렌더하므로 plain 이 기본이면
            // 최종본(streamReplace)에서 ## 헤더가 제거되어 제목/본문이 같은 크기로
            // 보이는 가독성 문제가 생긴다 (스트리밍 중에는 raw 가 그대로 렌더되어
            // 멀쩡하다가 완료 순간 평문으로 변하는 증상).
            const v = (cfg.get<string>('outputFormat', 'markdown') || 'markdown').trim().toLowerCase();
            return v === 'plain' ? 'plain' : 'markdown';
        })(),
        chronicleAutoRecord: cfg.get<boolean>('chronicleAutoRecord', true),
        lmStudioTopP: Math.max(0, Math.min(1, cfg.get<number>('lmStudio.sampling.topP', 0.9))),
        lmStudioTopK: Math.max(0, cfg.get<number>('lmStudio.sampling.topK', 20)),
        lmStudioMinP: Math.max(0, Math.min(1, cfg.get<number>('lmStudio.sampling.minP', 0.05))),
        lmStudioRepeatPenalty: Math.max(1, Math.min(2, cfg.get<number>('lmStudio.sampling.repeatPenalty', 1.1))),
        lmStudioShowStatsInBudget: cfg.get<boolean>('lmStudio.statsInBudget', true),
        lmStudioDraftModel: (cfg.get<string>('lmStudio.draftModel', '') || '').trim(),
        lmStudioLoad: {
            flashAttention: cfg.get<boolean>('lmStudio.load.flashAttention', true),
            gpuOffloadRatio: ((): 'max' | 'off' | number => {
                const raw = (cfg.get<string>('lmStudio.load.gpuOffloadRatio', 'max') || 'max').trim().toLowerCase();
                if (raw === 'max' || raw === 'off') return raw;
                const n = Number(raw);
                if (Number.isFinite(n)) return Math.max(0, Math.min(1, n));
                return 'max';
            })(),
            offloadKVCacheToGpu: cfg.get<boolean>('lmStudio.load.offloadKVCacheToGpu', true),
            keepModelInMemory: cfg.get<boolean>('lmStudio.load.keepModelInMemory', true),
            useFp16ForKVCache: cfg.get<boolean>('lmStudio.load.useFp16ForKVCache', false),
            evalBatchSize: Math.max(0, cfg.get<number>('lmStudio.load.evalBatchSize', 0)),
        },
    };
}

/**
 * Config Validator: Validates the current configuration.
 */
export function validateConfig(): { valid: boolean; errors: string[] } {
    const config = getConfig();
    const errors: string[] = [];

    // 1. Ollama URL Validation
    try {
        new URL(config.ollamaUrl);
    } catch (e) {
        errors.push(`Invalid Ollama URL: ${config.ollamaUrl}`);
    }

    // 2. Brain Path Validation
    if (config.localBrainPath && !fs.existsSync(config.localBrainPath)) {
        errors.push(`Brain path does not exist: ${config.localBrainPath}`);
    }

    return {
        valid: errors.length === 0,
        errors
    };
}

// ─── 보안 정책 (Security Policy) ───
export const SECURITY_POLICY = {
    allowedCommandPrefixes: [
        'npm', 'yarn', 'pnpm', 'npx', 'node', 'ts-node', 'git', 'python', 'python3', 'pip', 'pip3',
        'docker', 'docker-compose', 'ls', 'dir', 'cat', 'type', 'echo', 'print', 'cargo', 'go', 'rustc',
        'java', 'javac', 'mvn', 'gradle', 'flutter', 'dart', 'pub', 'webpack', 'vite', 'esbuild', 'parcel',
        'jest', 'mocha', 'vitest', 'cypress', 'tsc', 'vue-tsc',
    ],
    forbiddenCommands: [
        'rm -rf', 'rm-rf', 'del /f', 'format', 'mkfs', 'dd if=', ':(){ :|:& };:',
        'wget http', 'curl http', 'sudo', 'chmod 777', 'chown root',
    ],
    sensitiveFilePatterns: [
        '.env', '.env.*', 'id_rsa', 'id_ed25519', '.gitconfig', '.npmrc', '.pypirc',
        'credentials.json', 'service-account.json',
    ],
    maxFileSize: 10 * 1024 * 1024,
    maxContextFiles: 200,
};


export const EXCLUDED_DIRS = new Set([
    'node_modules', '.git', '.vscode', 'out', 'dist', 'build',
    '.next', '.cache', '__pycache__', '.DS_Store', 'coverage',
    '.turbo', '.nuxt', '.output', 'vendor', 'target', '.astra'
]);
