package com.zioinfo.cms.ai.service;

import com.zioinfo.cms.admin.AuditService;
import com.zioinfo.cms.admin.dto.CmsSetting;
import com.zioinfo.cms.admin.mapper.SettingMapper;
import com.zioinfo.cms.ai.OllamaClient;
import com.zioinfo.cms.ai.dto.AiConfigDto;
import com.zioinfo.cms.ai.dto.AiConfigUpdateRequest;
import com.zioinfo.cms.common.ai.ClaudeTextClient;
import com.zioinfo.cms.common.ai.TextAiClient.GenResult;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.util.Set;

/**
 * AI provider 런타임 설정(cms_setting, key='ai.*') 단일 출처 서비스. [GUARDiA-CMS]
 *
 * <p><b>해상도</b>: DB(cms_setting) 우선 → 미설정 시 기본값/env 폴백. 시드(db/104) 미적용/키 비움
 * 상태에서도 기존 Ollama 동작이 바이트 동일하게 유지된다(provider 기본 ollama, ollamaTextModel 미설정 시
 * {@code guardia.ollama-text-model} env 그대로).
 *
 * <p><b>보안</b>: Claude API 키는 본 서비스가 다루지 않는다. {@code claudeKeySet} 은
 * {@link ClaudeTextClient#isConfigured()}(환경변수 존재 여부)만 반환 — 값/길이/마스킹 일절 미포함.
 *
 * <p><b>프로바이더</b>: claude / qwen3 / deepseek / glm / ollama. Ollama 계열(qwen3·deepseek·glm·ollama)은
 * 각 해당 소형 텍스트 모델로 generate. claude 는 Claude→실패 시 Ollama(선택모델) 폴백(라우터).
 * glm(glm4:9b)은 화이트리스트에는 있으나 서버 RAM 초과 가능 → 콜드로드 실패 시 폴백(설정 화면 RAM 배지).
 *
 * <p><b>레퍼런스</b>: guardia-ocr {@code ai.service.AiConfigService} 미러(glm provider 확장).
 */
@Service
@RequiredArgsConstructor
public class AiConfigService {

    // --- cms_setting 키 (db/104 시드와 일치)
    public static final String K_PROVIDER = "ai.provider";               // ENUM: ollama|claude|qwen3|deepseek|glm
    public static final String K_CLAUDE_MODEL = "ai.claude.model";       // ENUM: 화이트리스트
    public static final String K_OLLAMA_TEXT_MODEL = "ai.ollama.textModel"; // STRING (미설정 시 env 폴백)
    public static final String K_ENABLED = "ai.enabled";                 // BOOL (off=규칙기반 degraded)

    public static final String PROVIDER_OLLAMA = "ollama";
    public static final String PROVIDER_CLAUDE = "claude";
    public static final String PROVIDER_QWEN3 = "qwen3";
    public static final String PROVIDER_DEEPSEEK = "deepseek";
    public static final String PROVIDER_GLM = "glm";
    public static final String DEFAULT_CLAUDE_MODEL = "claude-sonnet-4-6";

    // Ollama 계열 provider → 고정 소형 모델(서버 RAM 제약 준수)
    public static final String MODEL_QWEN3 = "qwen3:1.7b";
    public static final String MODEL_DEEPSEEK = "deepseek-r1:1.5b";
    public static final String MODEL_GLM = "glm4:9b";   // RAM 초과 가능(설정 화면 배지) — 콜드로드 실패 시 폴백

    /** 허용 provider(임의 문자열 거부). */
    public static final Set<String> ALLOWED_PROVIDERS =
            Set.of(PROVIDER_OLLAMA, PROVIDER_CLAUDE, PROVIDER_QWEN3, PROVIDER_DEEPSEEK, PROVIDER_GLM);
    /** 허용 Claude 모델 ID(임의 문자열 거부). */
    public static final Set<String> ALLOWED_CLAUDE_MODELS =
            Set.of("claude-sonnet-4-6", "claude-haiku-4-5", "claude-opus-4-8");
    /** 허용 Ollama 텍스트 모델(임의 문자열 거부). */
    public static final Set<String> ALLOWED_OLLAMA_MODELS =
            Set.of("qwen3:1.7b", "deepseek-r1:1.5b", "glm4:9b", "llama3.2:1b");

    private final SettingMapper repo;
    private final ClaudeTextClient claudeClient;
    private final OllamaClient ollamaClient;   // 연결 테스트용 텍스트 generate
    private final AuditService auditService;

    /** 서버 기본 Ollama 텍스트 모델(application.yml guardia.ollama-text-model). */
    @Value("${guardia.ollama-text-model:llama3.2:1b}")
    private String defaultOllamaTextModel;

    // ---------------------------------------------------------------- 효과값(DB 우선 → 기본)

    /** 효과 provider. DB 우선, 미설정/비허용 시 ollama. */
    public String provider() {
        String v = dbVal(K_PROVIDER);
        if (v != null) {
            String p = v.trim().toLowerCase();
            if (ALLOWED_PROVIDERS.contains(p)) {
                return p;
            }
        }
        return PROVIDER_OLLAMA;
    }

    /** 효과 Claude 모델 ID(DB 우선, 미설정/비허용 시 기본 sonnet-4-6). */
    public String claudeModel() {
        String v = dbVal(K_CLAUDE_MODEL);
        if (v != null) {
            String m = v.trim();
            if (ALLOWED_CLAUDE_MODELS.contains(m)) {
                return m;
            }
        }
        return DEFAULT_CLAUDE_MODEL;
    }

    /**
     * 효과 Ollama 텍스트 모델. provider=qwen3/deepseek/glm 이면 고정 모델, 그 외(ollama·claude 폴백)는
     * DB 설정(화이트리스트) 우선 → 미설정 시 env 기본(llama3.2:1b). 라우터의 Ollama 경로·Claude 폴백 공용.
     */
    public String ollamaTextModel() {
        String p = provider();
        if (PROVIDER_QWEN3.equals(p)) {
            return MODEL_QWEN3;
        }
        if (PROVIDER_DEEPSEEK.equals(p)) {
            return MODEL_DEEPSEEK;
        }
        if (PROVIDER_GLM.equals(p)) {
            return MODEL_GLM;
        }
        String v = dbVal(K_OLLAMA_TEXT_MODEL);
        if (v != null && ALLOWED_OLLAMA_MODELS.contains(v.trim())) {
            return v.trim();
        }
        return defaultOllamaTextModel;
    }

    /** 전역 AI 토글(ai.enabled). false 면 모든 AI 결과는 규칙기반(degraded). 기본 true. */
    public boolean aiEnabled() {
        String v = dbVal(K_ENABLED);
        return v == null || !"false".equalsIgnoreCase(v.trim());
    }

    /** Claude API 키가 환경변수로 설정되어 있는지(여부만). */
    public boolean claudeKeySet() {
        return claudeClient.isConfigured();
    }

    /**
     * 실제 호출이 Claude 경로로 가야 하는지: provider=claude · 키 설정됨 · AI 전역 활성.
     * false 면 호출자는 Ollama 경로/폴백을 사용한다(키 미설정/실패 시 자동 폴백 정책 반영).
     */
    public boolean isClaudeActive() {
        return PROVIDER_CLAUDE.equals(provider()) && claudeKeySet() && aiEnabled();
    }

    // ---------------------------------------------------------------- ADMIN: 조회 / 갱신

    /** 현재 효과 설정 조회. 키 값·길이·마스킹 일절 미포함(claudeKeySet 불리언만). */
    public AiConfigDto getConfig() {
        return new AiConfigDto(
                provider(),
                ollamaTextModel(),
                claudeModel(),
                claudeKeySet(),
                aiEnabled());
    }

    /**
     * 설정 갱신(upsert). 화이트리스트 검증: provider∈{ollama,claude,qwen3,deepseek,glm}, claudeModel∈허용셋,
     * ollamaTextModel(선택)∈허용셋. 재기동 없이 다음 호출부터 반영.
     */
    public AiConfigDto update(AiConfigUpdateRequest req, String actor) {
        String provider = req.provider() == null ? "" : req.provider().trim().toLowerCase();
        if (!ALLOWED_PROVIDERS.contains(provider)) {
            throw new IllegalArgumentException("ERR-AI-400: provider 는 ollama/claude/qwen3/deepseek/glm 중 하나여야 합니다.");
        }
        upsertAudited(K_PROVIDER, provider, actor);

        if (req.claudeModel() != null && !req.claudeModel().isBlank()) {
            String model = req.claudeModel().trim();
            if (!ALLOWED_CLAUDE_MODELS.contains(model)) {
                throw new IllegalArgumentException("ERR-AI-400: 허용되지 않은 Claude 모델 ID 입니다.");
            }
            upsertAudited(K_CLAUDE_MODEL, model, actor);
        }

        // ollamaTextModel 은 선택. 제공 시에만 갱신(미제공/공백 = 기존 유지).
        if (req.ollamaTextModel() != null && !req.ollamaTextModel().isBlank()) {
            String om = req.ollamaTextModel().trim();
            if (!ALLOWED_OLLAMA_MODELS.contains(om)) {
                throw new IllegalArgumentException("ERR-AI-400: 허용되지 않은 Ollama 텍스트 모델입니다.");
            }
            upsertAudited(K_OLLAMA_TEXT_MODEL, om, actor);
        }
        return getConfig();
    }

    // ---------------------------------------------------------------- ADMIN: 연결 테스트

    /**
     * 저장된 설정 기준으로 선택 provider 에 짧은 ping 생성을 요청해 연결을 확인한다.
     * 결과 메시지에는 키·스택트레이스·내부 IP 를 절대 포함하지 않는다(요약만).
     */
    public TestResult test() {
        if (isClaudeActive()) {
            String model = claudeModel();
            long start = System.currentTimeMillis();
            GenResult r = claudeClient.generate("ping", model, 8);
            long ms = System.currentTimeMillis() - start;
            if (!r.degraded() && r.text() != null && !r.text().isBlank()) {
                return new TestResult(true, false, "정상 · " + model + " · " + fmtMs(ms));
            }
            return new TestResult(false, true, "Claude 연결 실패 · 모델/네트워크/키 설정을 확인하세요.");
        }
        // provider=claude 인데 키 미설정 → 실제로는 Ollama 폴백 동작 안내(혼선 방지).
        if (PROVIDER_CLAUDE.equals(provider()) && !claudeKeySet()) {
            return new TestResult(false, true,
                    "Claude API 키가 설정되지 않아 Ollama(온프레미스)로 폴백 동작합니다. 서버 환경변수 설정 후 다시 시도하세요.");
        }
        // Ollama 계열 경로(ollama/qwen3/deepseek/glm)
        if (!aiEnabled()) {
            return new TestResult(false, true, "AI 기능이 비활성 상태입니다. 모든 AI 결과는 규칙 기반(degraded)으로 동작합니다.");
        }
        String model = ollamaTextModel();
        long start = System.currentTimeMillis();
        String txt = ollamaClient.generate("ping", model);
        long ms = System.currentTimeMillis() - start;
        if (txt != null && !txt.isBlank()) {
            return new TestResult(true, false, "정상 · " + model + " · " + fmtMs(ms));
        }
        return new TestResult(false, true, "Ollama 연결 실패 또는 모델 미가용 — 설정을 확인하세요.");
    }

    private static String fmtMs(long ms) {
        return String.format("%.1fs", ms / 1000.0);
    }

    /** 연결 테스트 결과(요약만 — 키/스택/IP 미노출). */
    public record TestResult(boolean ok, boolean degraded, String message) {
    }

    // ---------------------------------------------------------------- helpers

    /** DB 값(공백/null 은 '미설정'으로 보고 null 반환 → 기본/env 폴백 유도). */
    private String dbVal(String key) {
        CmsSetting c = repo.findByKey(key);
        if (c == null) {
            return null;
        }
        String v = c.getValue();
        return (v != null && !v.isBlank()) ? v : null;
    }

    private void upsertAudited(String key, String val, String actor) {
        CmsSetting before = repo.findByKey(key);
        String prev = before == null ? "(none)" : before.getValue();
        if (val.equals(prev)) {
            return; // 변경 없음 → 감사 로그 생략
        }
        repo.upsert(key, val);
        auditService.log(actor == null ? "SYSTEM" : actor, "AI_CONFIG_CHANGE", key, prev + " -> " + val);
    }
}
