package com.zioinfo.mall.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.time.LocalDateTime;
import java.util.regex.Pattern;

/**
 * GUARDiA Mall 로컬 임베디드 DuckDB 학습 저장소. [GUARDiA-MALL]
 *
 * <p>AI 피드백·추론 로그를 솔루션 로컬 파일({@code /opt/guardia-mall/data/mall_learning.duckdb})에 격리
 * 저장한다(오프라인 분석·학습 데이터셋). 중앙 guardia-rag(8020) 로의 전달은 {@code AiFeedbackController} 가
 * 별도로 수행한다(둘 다 기록).
 *
 * <p><b>내결함성(불변)</b>: DuckDB 초기화/기록 실패는 절대 예외를 전파하지 않는다(AI 본기능·서비스 무영향).
 * 초기화 실패 시 {@code enabled=false} 로 no-op 동작한다. 로컬 컴파일 환경에 경로가 없어도 안전.
 *
 * <p><b>PII 마스킹(불변)</b>: question·answer·correction·user_ref 는 저장 전 이메일/전화/카드/주민번호를
 * 마스킹한다. 자격증명·카드·회원 PII 원문을 파일에 남기지 않는다.
 *
 * <p>스키마(멱등): {@code ai_feedback(id,ts,solution,feature,question,answer,verdict,correction,user_masked)},
 * {@code ai_infer_log(id,ts,provider,model,latency_ms,degraded)}.
 */
@Slf4j
@Component
public class MallLearningStore {

    private static final String SOLUTION = "mall";

    private static final Pattern EMAIL = Pattern.compile("[A-Za-z0-9._%+\\-]+@[A-Za-z0-9.\\-]+\\.[A-Za-z]{2,}");
    private static final Pattern CARD = Pattern.compile("\\b(?:\\d[ \\-]?){13,16}\\b");
    private static final Pattern PHONE = Pattern.compile("\\b(?:\\+?\\d{1,3}[\\-. ]?)?(?:\\(?\\d{2,4}\\)?[\\-. ]?)\\d{3,4}[\\-. ]?\\d{4}\\b");
    private static final Pattern SSN = Pattern.compile("\\b\\d{6}[\\-]?\\d{7}\\b");

    private final String dbPath;
    private volatile boolean enabled = false;
    private Connection conn;

    public MallLearningStore(
            @Value("${guardia.mall.learning.duckdb-path:/opt/guardia-mall/data/mall_learning.duckdb}") String dbPath) {
        this.dbPath = dbPath;
    }

    /** 부팅 시 DuckDB 연결·스키마 멱등 생성. 실패해도 서비스는 정상(enabled=false). */
    @PostConstruct
    public synchronized void init() {
        try {
            File f = new File(dbPath);
            File parent = f.getParentFile();
            if (parent != null && !parent.exists() && !parent.mkdirs()) {
                log.warn("Mall learning store: data dir 생성 불가 -> 로컬 학습저장소 비활성");
                return;
            }
            Class.forName("org.duckdb.DuckDBDriver");
            this.conn = DriverManager.getConnection("jdbc:duckdb:" + dbPath);
            try (Statement st = conn.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)");
            }
            this.enabled = true;
            log.info("Mall learning store(DuckDB) 활성: {}", dbPath);
        } catch (Throwable t) {  // 드라이버 부재·경로 불가 등 모든 오류를 흡수(서비스 무영향)
            this.enabled = false;
            log.warn("Mall learning store(DuckDB) 초기화 실패 -> 비활성(no-op): {}", t.getClass().getSimpleName());
        }
    }

    public boolean isEnabled() {
        return enabled;
    }

    /** 피드백 로컬 기록(PII 마스킹). 실패해도 예외 미전파. */
    public synchronized void recordFeedback(String feature, String question, String answer,
                                            String verdict, String correction, String userRef) {
        if (!enabled || conn == null) return;
        try (PreparedStatement ps = conn.prepareStatement(
                "INSERT INTO ai_feedback (id,ts,solution,feature,question,answer,verdict,correction,user_masked) "
                        + "VALUES (nextval('seq_ai_feedback'),?,?,?,?,?,?,?,?)")) {
            ps.setObject(1, LocalDateTime.now());
            ps.setString(2, SOLUTION);
            ps.setString(3, trunc(feature, 120));
            ps.setString(4, mask(trunc(question, 4000)));
            ps.setString(5, mask(trunc(answer, 8000)));
            ps.setString(6, trunc(verdict, 16));
            ps.setString(7, mask(trunc(correction, 8000)));
            ps.setString(8, mask(trunc(userRef, 120)));
            ps.executeUpdate();
        } catch (Throwable t) {
            log.warn("Mall learning store: feedback 기록 실패(무시): {}", t.getClass().getSimpleName());
        }
    }

    /** 추론 로그 기록(provider/model/지연/degraded). 실패해도 예외 미전파. */
    public synchronized void recordInfer(String provider, String model, long latencyMs, boolean degraded) {
        if (!enabled || conn == null) return;
        try (PreparedStatement ps = conn.prepareStatement(
                "INSERT INTO ai_infer_log (id,ts,provider,model,latency_ms,degraded) "
                        + "VALUES (nextval('seq_ai_infer_log'),?,?,?,?,?)")) {
            ps.setObject(1, LocalDateTime.now());
            ps.setString(2, trunc(provider, 40));
            ps.setString(3, trunc(model, 60));
            ps.setLong(4, Math.max(0, latencyMs));
            ps.setBoolean(5, degraded);
            ps.executeUpdate();
        } catch (Throwable t) {
            log.warn("Mall learning store: infer 로그 실패(무시): {}", t.getClass().getSimpleName());
        }
    }

    @PreDestroy
    public synchronized void close() {
        if (conn != null) {
            try { conn.close(); } catch (Exception ignore) { /* no-op */ }
        }
    }

    // ── helpers ───────────────────────────────────────────────────────────
    /** 이메일/카드/전화/주민번호 마스킹(원문 미저장). */
    static String mask(String s) {
        if (s == null || s.isBlank()) return s;
        String out = s;
        out = SSN.matcher(out).replaceAll("######-#######");
        out = CARD.matcher(out).replaceAll("****-****-****-****");
        out = EMAIL.matcher(out).replaceAll("***@***");
        out = PHONE.matcher(out).replaceAll("***-****-****");
        return out;
    }

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