package com.zioinfo.cms.ai.learning;

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.util.regex.Pattern;

/**
 * 로컬 임베디드 DuckDB AI 학습/피드백 저장소. [GUARDiA-CMS]
 *
 * <p>스펙(AI_PLATFORM_SPEC §5): 각 솔루션은 로컬 DuckDB 파일을 AI 피드백·추론로그·학습 데이터셋
 * 저장소로 사용한다. 파일 = {@code /opt/guardia-cms/data/cms_learning.duckdb}(솔루션별 격리).
 * 테이블(멱등): {@code ai_feedback}, {@code ai_infer_log}.
 *
 * <p><b>안전(최우선)</b>: 본 저장소는 절대 요청 흐름을 끊지 않는다. 연결/DDL/INSERT 실패 시 예외를
 * 전파하지 않고 경고만 남기며 disabled 로 degrade 한다(피드백 UI·라우터는 정상 동작 유지). 연결은
 * 최초 사용 시 지연 오픈(부팅 실패 방지). 파일 부모 디렉터리가 없으면 생성 시도.
 *
 * <p><b>PII 보호</b>: 저장 전 {@link #maskPii(String)} 로 주민번호/카드번호/전화번호/이메일 마스킹.
 * 자격증명/비밀번호/내부 IP 는 애초에 수집하지 않는다.
 */
@Slf4j
@Component
public class LearningStore {

    private static final String SOLUTION = "cms";

    private final String dbPath;
    private volatile Connection conn;      // 지연 오픈 단일 연결(쓰기 직렬화)
    private volatile boolean disabled;     // 오픈/DDL 실패 시 true — 이후 조용히 no-op
    private final Object lock = new Object();

    public LearningStore(@Value("${guardia.duckdb.path:/opt/guardia-cms/data/cms_learning.duckdb}") String dbPath) {
        this.dbPath = dbPath;
    }

    /** 저장소 가용 여부(연결 확보 시도 포함). */
    public boolean available() {
        return connection() != null;
    }

    /** 피드백 저장(👍/👎 + 교정). 실패는 무시(best-effort). */
    public void insertFeedback(String feature, String question, String answer,
                               String verdict, String correction, String userRef) {
        Connection c = connection();
        if (c == null) return;
        String sql = "INSERT INTO ai_feedback "
                + "(id, ts, solution, feature, question, answer, verdict, correction, user_masked) "
                + "VALUES (nextval('seq_ai_feedback'), CURRENT_TIMESTAMP, ?, ?, ?, ?, ?, ?, ?)";
        synchronized (lock) {
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                ps.setString(1, SOLUTION);
                ps.setString(2, trunc(feature, 120));
                ps.setString(3, maskPii(trunc(question, 4000)));
                ps.setString(4, maskPii(trunc(answer, 4000)));
                ps.setString(5, normVerdict(verdict));
                ps.setString(6, maskPii(trunc(correction, 4000)));
                ps.setString(7, maskPii(trunc(userRef, 200)));
                ps.executeUpdate();
            } catch (Exception e) {
                log.warn("DuckDB ai_feedback INSERT 실패(무시): {}", e.getClass().getSimpleName());
            }
        }
    }

    /** 추론 로그(provider/model/지연/degraded). 실패는 무시(best-effort). */
    public void logInfer(String provider, String model, long latencyMs, boolean degraded) {
        Connection c = connection();
        if (c == null) return;
        String sql = "INSERT INTO ai_infer_log (id, ts, provider, model, latency_ms, degraded) "
                + "VALUES (nextval('seq_ai_infer_log'), CURRENT_TIMESTAMP, ?, ?, ?, ?)";
        synchronized (lock) {
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                ps.setString(1, trunc(provider, 40));
                ps.setString(2, trunc(model, 80));
                ps.setLong(3, latencyMs);
                ps.setBoolean(4, degraded);
                ps.executeUpdate();
            } catch (Exception e) {
                log.warn("DuckDB ai_infer_log INSERT 실패(무시): {}", e.getClass().getSimpleName());
            }
        }
    }

    // ---------------------------------------------------------------- 지연 오픈 + 멱등 DDL

    private Connection connection() {
        if (disabled) return null;
        Connection existing = conn;
        if (existing != null) return existing;
        synchronized (lock) {
            if (disabled) return null;
            if (conn != null) return conn;
            try {
                File f = new File(dbPath);
                File dir = f.getParentFile();
                if (dir != null && !dir.exists() && !dir.mkdirs()) {
                    log.warn("DuckDB 데이터 디렉터리 생성 불가 — 로컬 학습저장소 비활성: {}", dir.getPath());
                    disabled = true;
                    return null;
                }
                // DuckDB JDBC 드라이버 로드(명시) → 연결 오픈 → 멱등 DDL.
                Class.forName("org.duckdb.DuckDBDriver");
                Connection c = DriverManager.getConnection("jdbc:duckdb:" + dbPath);
                initSchema(c);
                conn = c;
                log.info("DuckDB 로컬 학습저장소 준비 완료: {}", dbPath);
                return conn;
            } catch (Throwable t) {
                log.warn("DuckDB 로컬 학습저장소 비활성(무시): {}", t.getClass().getSimpleName());
                disabled = true;
                return null;
            }
        }
    }

    private void initSchema(Connection c) throws Exception {
        try (Statement st = c.createStatement()) {
            st.execute("CREATE SEQUENCE IF NOT EXISTS seq_ai_feedback START 1");
            st.execute("CREATE SEQUENCE IF NOT EXISTS seq_ai_infer_log START 1");
            st.execute("CREATE TABLE IF NOT EXISTS ai_feedback ("
                    + "id BIGINT PRIMARY KEY, ts TIMESTAMP, solution VARCHAR, feature VARCHAR, "
                    + "question VARCHAR, answer VARCHAR, verdict VARCHAR, correction VARCHAR, user_masked VARCHAR)");
            st.execute("CREATE TABLE IF NOT EXISTS ai_infer_log ("
                    + "id BIGINT PRIMARY KEY, ts TIMESTAMP, provider VARCHAR, model VARCHAR, "
                    + "latency_ms BIGINT, degraded BOOLEAN)");
        }
    }

    @PreDestroy
    void close() {
        synchronized (lock) {
            if (conn != null) {
                try { conn.close(); } catch (Exception ignore) { }
                conn = null;
            }
        }
    }

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

    private static final Pattern RRN   = Pattern.compile("\\b\\d{6}[- ]?\\d{7}\\b");             // 주민번호
    private static final Pattern CARD  = Pattern.compile("\\b(?:\\d[ -]?){13,16}\\b");           // 카드/계좌
    private static final Pattern PHONE = Pattern.compile("\\b0\\d{1,2}[- ]?\\d{3,4}[- ]?\\d{4}\\b"); // 전화
    private static final Pattern EMAIL = Pattern.compile("[\\w.+-]+@[\\w-]+\\.[\\w.-]+");         // 이메일

    /** 저장 전 PII 마스킹(주민/카드/전화/이메일). null 안전. */
    static String maskPii(String s) {
        if (s == null || s.isBlank()) return s;
        String out = RRN.matcher(s).replaceAll("######-#######");
        out = CARD.matcher(out).replaceAll("****-****-****-****");
        out = PHONE.matcher(out).replaceAll("***-****-****");
        out = EMAIL.matcher(out).replaceAll("***@***");
        return out;
    }

    private static String normVerdict(String v) {
        if (v == null) return "unknown";
        String t = v.trim().toLowerCase();
        return switch (t) {
            case "up", "good", "positive", "👍" -> "up";
            case "down", "bad", "negative", "👎" -> "down";
            default -> t.isEmpty() ? "unknown" : trunc(t, 20);
        };
    }

    private static String trunc(String s, int max) {
        if (s == null) return null;
        return s.length() <= max ? s : s.substring(0, max);
    }
}
