package com.zioinfo.esn.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;

/**
 * 로컬 임베디드 DuckDB 학습/분석 저장소. [GUARDiA-ESN]
 *
 * <p>AI 피드백(👍/👎+교정)과 추론 로그(provider/model/latency/degraded)를 솔루션별 로컬 DuckDB 파일에
 * 격리 저장한다(오프라인 AI 분석용). 중앙 guardia-rag {@code /feedback}(통합 학습·평가 게이트)와는
 * <b>병행</b>이며 서로를 대체하지 않는다(피드백은 로컬 + 중앙 둘 다 기록).
 *
 * <p><b>파일</b>: {@code guardia.learning.duckdb-path}(기본 {@code /opt/guardia-esn/data/esn_learning.duckdb}).
 * 스키마는 부팅 시 멱등 생성({@code CREATE TABLE/SEQUENCE IF NOT EXISTS}).
 *
 * <p><b>Fail-Safe</b>: DuckDB 초기화/기록 실패는 <b>본업을 막지 않는다</b>(예외 흡수·no-op).
 * 파일 미생성/드라이버 부재/권한 문제 시 {@code available=false} 로 조용히 비활성(로그 1줄만).
 * DuckDB 는 프로세스당 단일 R/W 연결이므로 하나의 {@link Connection} 을 락으로 직렬화한다.
 *
 * <p><b>보안 불변</b>: 저장 전 PII(주민번호·카드·전화·이메일) 마스킹. 사용자 식별자는 앞 2자만 남기고 마스킹.
 * 자격증명/비밀번호/스택트레이스는 어떤 컬럼에도 기록하지 않는다(호출 측이 요약만 전달).
 */
@Slf4j
@Component
public class LocalLearningStore {

    @Value("${guardia.learning.duckdb-path:/opt/guardia-esn/data/esn_learning.duckdb}")
    private String dbPath;

    @Value("${guardia.rag.solution:esn}")
    private String solution;

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

    // PII 패턴(마스킹) — 최소 집합. 과탐보다 미기록이 안전.
    private static final Pattern RRN = Pattern.compile("\\b\\d{6}[- ]?\\d{7}\\b");
    private static final Pattern CARD = Pattern.compile("\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b");
    private static final Pattern PHONE = Pattern.compile("\\b01[016789][- ]?\\d{3,4}[- ]?\\d{4}\\b");
    private static final Pattern EMAIL = Pattern.compile("[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}");
    private static final int MAX_LEN = 4000;

    @PostConstruct
    void init() {
        try {
            File f = new File(dbPath);
            File dir = f.getParentFile();
            if (dir != null && !dir.exists() && !dir.mkdirs()) {
                log.warn("DuckDB 디렉터리 생성 불가 — 로컬 학습 저장소 비활성: {}", dir);
                return;
            }
            Class.forName("org.duckdb.DuckDBDriver");
            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, 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, ts TIMESTAMP, provider VARCHAR, model VARCHAR, latency_ms BIGINT, degraded BOOLEAN)");
            }
            available = true;
            log.info("로컬 DuckDB 학습 저장소 준비 완료: {}", dbPath);
        } catch (Throwable t) {
            // 드라이버 부재/권한/파일락 등 — 본업 무영향. 조용히 비활성(스택 미노출).
            available = false;
            log.warn("로컬 DuckDB 학습 저장소 비활성(무영향): {}", t.getClass().getSimpleName());
        }
    }

    public boolean isAvailable() {
        return available;
    }

    /** 피드백 1건 기록(멱등 아님 — append). PII 마스킹 후 저장. 실패는 흡수. */
    public void recordFeedback(String feature, String question, String answer,
                               String verdict, String correction, String user) {
        if (!available) {
            return;
        }
        synchronized (lock) {
            try (PreparedStatement ps = conn.prepareStatement(
                    "INSERT INTO ai_feedback VALUES (nextval('seq_ai_feedback'), ?, ?, ?, ?, ?, ?, ?, ?)")) {
                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, maskUser(user));
                ps.executeUpdate();
            } catch (Exception e) {
                log.warn("DuckDB ai_feedback 기록 실패(무영향): {}", e.getClass().getSimpleName());
            }
        }
    }

    /** 추론 로그 1건 기록(provider/model/latency/degraded). 실패는 흡수. */
    public void recordInfer(String provider, String model, long latencyMs, boolean degraded) {
        if (!available) {
            return;
        }
        synchronized (lock) {
            try (PreparedStatement ps = conn.prepareStatement(
                    "INSERT INTO ai_infer_log VALUES (nextval('seq_ai_infer_log'), ?, ?, ?, ?, ?)")) {
                ps.setTimestamp(1, Timestamp.from(Instant.now()));
                ps.setString(2, clip(provider));
                ps.setString(3, clip(model));
                ps.setLong(4, latencyMs);
                ps.setBoolean(5, degraded);
                ps.executeUpdate();
            } catch (Exception e) {
                log.warn("DuckDB ai_infer_log 기록 실패(무영향): {}", e.getClass().getSimpleName());
            }
        }
    }

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

    // ── PII 마스킹 ─────────────────────────────────────────────────────────
    static String mask(String s) {
        if (s == null || s.isBlank()) {
            return s;
        }
        String out = s;
        out = RRN.matcher(out).replaceAll("######-#######");
        out = CARD.matcher(out).replaceAll("****-****-****-****");
        out = PHONE.matcher(out).replaceAll("***-****-****");
        out = EMAIL.matcher(out).replaceAll("***@***");
        return clip(out);
    }

    /** 사용자 식별자 마스킹: 앞 2자만 노출. */
    public static String maskUser(String u) {
        if (u == null || u.isBlank()) {
            return null;
        }
        String t = u.trim();
        if (t.length() <= 2) {
            return t.charAt(0) + "*";
        }
        return t.substring(0, 2) + "***";
    }

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