package com.zioinfo.mes.ai.learning;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.io.File;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.Statement;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.regex.Pattern;

/**
 * MES AI 로컬 학습 저장소 — 임베디드 <b>DuckDB</b>(단일 파일). [GUARDiA-MES]
 *
 * <p>AI 피드백(👍/👎+교정)과 추론 로그를 솔루션 로컬 DuckDB 파일에 적재해 오프라인 AI 분석/학습 데이터셋으로
 * 사용한다(중앙 guardia-rag 로의 전달은 {@code AiFeedbackController} 가 별도로 수행 — 이 클래스는 로컬 적재만).
 *
 * <p>스키마(멱등):
 * <ul>
 *   <li>{@code ai_feedback(id, ts, solution, feature, question, answer, verdict, correction, user_masked)}</li>
 *   <li>{@code ai_infer_log(id, ts, provider, model, latency_ms, degraded)}</li>
 * </ul>
 *
 * <p><b>안전성</b>: 드라이버는 리플렉션 로드(컴파일 타임 의존 0). 파일/디렉터리 미가용(로컬 개발 등)이면
 * {@code enabled=false} 로 조용히 비활성 — 절대 예외를 던지지 않아 AI 기능은 그대로 동작한다.
 * <b>보안</b>: 저장 전 PII(주민번호·카드·전화·이메일)·자격증명 마스킹. 솔루션별 파일 격리.
 */
@Slf4j
@Component
public class AiLearningStore {

    private static final String SOLUTION = "mes";

    // PII 마스킹 패턴(수집 전 적용)
    private static final Pattern RRN = Pattern.compile("\\d{6}[- ]?\\d{7}");
    private static final Pattern CARD = Pattern.compile("\\b(?:\\d[ -]?){13,16}\\b");
    private static final Pattern PHONE = Pattern.compile("01[016789][- ]?\\d{3,4}[- ]?\\d{4}");
    private static final Pattern EMAIL = Pattern.compile("[\\w.+-]+@[\\w.-]+\\.[A-Za-z]{2,}");
    private static final int MAX_TEXT = 4000;

    @Value("${mes.ai.duckdb-path:/opt/guardia-mes/data/mes_learning.duckdb}")
    private String dbPath;

    private volatile boolean enabled = false;
    private Connection conn;
    private final Object lock = new Object();

    @PostConstruct
    void init() {
        try {
            Class.forName("org.duckdb.DuckDBDriver");
            File f = new File(dbPath);
            File dir = f.getParentFile();
            if (dir != null && !dir.exists() && !dir.mkdirs()) {
                log.warn("AiLearningStore(DuckDB) 비활성 — 디렉터리 생성 불가(로컬 AI 분석 저장 생략, AI 기능은 정상)");
                return;
            }
            conn = DriverManager.getConnection("jdbc:duckdb:" + dbPath);
            try (Statement st = conn.createStatement()) {
                st.execute("CREATE SEQUENCE IF NOT EXISTS ai_feedback_seq START 1");
                st.execute("CREATE TABLE IF NOT EXISTS ai_feedback (" +
                        "id BIGINT, ts TIMESTAMP, solution VARCHAR, feature VARCHAR, question VARCHAR, " +
                        "answer VARCHAR, verdict VARCHAR, correction VARCHAR, user_masked VARCHAR)");
                st.execute("CREATE SEQUENCE IF NOT EXISTS ai_infer_log_seq START 1");
                st.execute("CREATE TABLE IF NOT EXISTS ai_infer_log (" +
                        "id BIGINT, ts TIMESTAMP, provider VARCHAR, model VARCHAR, latency_ms BIGINT, degraded BOOLEAN)");
            }
            enabled = true;
            log.info("AiLearningStore(DuckDB) 준비 완료 (solution=mes)");
        } catch (Throwable t) {
            enabled = false;   // duckdb 미로딩/파일 미가용 — 조용히 비활성(AI 기능 무영향)
            log.warn("AiLearningStore(DuckDB) 비활성 ({}) — 로컬 AI 학습 저장 생략, AI 기능은 정상 동작",
                    t.getClass().getSimpleName());
        }
    }

    public boolean isEnabled() {
        return enabled;
    }

    /** 추론 1건 로그(provider/model/지연/폴백여부). 실패해도 예외 없음. */
    public void logInfer(String provider, String model, long latencyMs, boolean degraded) {
        if (!enabled) {
            return;
        }
        synchronized (lock) {
            try (PreparedStatement ps = conn.prepareStatement(
                    "INSERT INTO ai_infer_log VALUES (nextval('ai_infer_log_seq'), ?, ?, ?, ?, ?)")) {
                ps.setTimestamp(1, Timestamp.from(Instant.now()));
                ps.setString(2, provider);
                ps.setString(3, model);
                ps.setLong(4, Math.max(0, latencyMs));
                ps.setBoolean(5, degraded);
                ps.executeUpdate();
            } catch (Throwable t) {
                log.debug("ai_infer_log insert skip: {}", t.getClass().getSimpleName());
            }
        }
    }

    /** 피드백 1건 저장(로컬). PII 마스킹 후 적재. 실패해도 예외 없음. */
    public void saveFeedback(String feature, String question, String answer,
                             String verdict, String correction, String actor) {
        if (!enabled) {
            return;
        }
        synchronized (lock) {
            try (PreparedStatement ps = conn.prepareStatement(
                    "INSERT INTO ai_feedback VALUES (nextval('ai_feedback_seq'), ?, ?, ?, ?, ?, ?, ?, ?)")) {
                ps.setTimestamp(1, Timestamp.from(Instant.now()));
                ps.setString(2, SOLUTION);
                ps.setString(3, clip(feature));
                ps.setString(4, mask(question));
                ps.setString(5, mask(answer));
                ps.setString(6, clip(verdict));
                ps.setString(7, mask(correction));
                ps.setString(8, maskActor(actor));
                ps.executeUpdate();
            } catch (Throwable t) {
                log.debug("ai_feedback insert skip: {}", t.getClass().getSimpleName());
            }
        }
    }

    @PreDestroy
    void close() {
        if (conn != null) {
            try {
                conn.close();
            } catch (Throwable ignored) {
                // best-effort
            }
        }
    }

    // ── PII/자격증명 마스킹 ──
    static String mask(String s) {
        if (s == null || s.isBlank()) {
            return null;
        }
        String v = s.length() > MAX_TEXT ? s.substring(0, MAX_TEXT) : s;
        v = RRN.matcher(v).replaceAll("######-#######");
        v = CARD.matcher(v).replaceAll("****-****-****-****");
        v = PHONE.matcher(v).replaceAll("***-****-****");
        v = EMAIL.matcher(v).replaceAll("***@***");
        return v;
    }

    /** 작성자 식별자는 앞 2자만 남기고 마스킹(감사 최소화). */
    static String maskActor(String actor) {
        if (actor == null || actor.isBlank()) {
            return "anon";
        }
        String a = actor.trim();
        return a.length() <= 2 ? a.charAt(0) + "*" : a.substring(0, 2) + "***";
    }

    private static String clip(String s) {
        if (s == null) {
            return null;
        }
        return s.length() > 200 ? s.substring(0, 200) : s;
    }
}
