package com.zioinfo.kintex.common.text;

import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * CMS 본문 XSS 방어(N2) — 허용 태그 화이트리스트 기반 서버 sanitize.
 * <p>외부 의존성(jsoup 등) 없이 순수 정규식 전략으로 위험 요소를 제거한다.
 * <ul>
 *   <li>{@code <script>}·{@code <style>}·{@code <iframe>}·{@code <object>}·{@code <embed>} 등 위험 태그는 내용째 제거.</li>
 *   <li>{@code on*} 인라인 이벤트 핸들러 속성 제거(onclick·onerror·onload …).</li>
 *   <li>{@code javascript:}·{@code data:}·{@code vbscript:} 스킴 href/src 제거.</li>
 *   <li>화이트리스트에 없는 태그는 태그만 제거하고 <b>텍스트 내용은 보존</b>(escape 아님).</li>
 * </ul>
 * 에디터는 블록/리치텍스트(제한 서식)이므로 본문은 서식 태그 소집합만 허용한다.
 */
public final class HtmlSanitizer {

    /** 허용 태그(여닫기 공통) — 리치텍스트 최소 서식. */
    private static final Set<String> ALLOWED = Set.of(
            "p", "br", "b", "strong", "i", "em", "u", "s",
            "ul", "ol", "li", "blockquote", "code", "pre",
            "h1", "h2", "h3", "h4", "a", "span", "div");

    /** 내용째 삭제할 위험 태그(여는 태그 ~ 닫는 태그 전체). */
    private static final Pattern DANGEROUS_BLOCKS = Pattern.compile(
            "(?is)<\\s*(script|style|iframe|object|embed|form|svg|math|template|noscript)\\b.*?<\\s*/\\s*\\1\\s*>");
    /** 짝이 맞지 않는 위험 여는/닫는 태그 잔재. */
    private static final Pattern DANGEROUS_SOLO = Pattern.compile(
            "(?is)<\\s*/?\\s*(script|style|iframe|object|embed|form|svg|math|template|noscript)\\b[^>]*>");
    /** on* 인라인 이벤트 핸들러 속성. */
    private static final Pattern EVENT_ATTR = Pattern.compile(
            "(?is)\\s+on[a-z]+\\s*=\\s*(\"[^\"]*\"|'[^']*'|[^\\s>]+)");
    /** 위험 스킴 URL 속성(href/src/xlink:href). */
    private static final Pattern DANGEROUS_URL = Pattern.compile(
            "(?is)\\s+(href|src|xlink:href)\\s*=\\s*(\"\\s*(javascript|data|vbscript):[^\"]*\"|'\\s*(javascript|data|vbscript):[^']*'|(javascript|data|vbscript):[^\\s>]+)");
    /** 일반 태그 매칭(여는/닫는). */
    private static final Pattern ANY_TAG = Pattern.compile("(?is)<\\s*(/?)\\s*([a-z][a-z0-9]*)\\b[^>]*>");

    private HtmlSanitizer() {
    }

    /**
     * 본문을 안전하게 정화한다. null/blank 는 그대로 반환.
     * 최대 길이(200,000자)를 초과하면 잘라낸다(폭주 방어).
     */
    public static String sanitize(String raw) {
        if (raw == null || raw.isEmpty()) {
            return raw;
        }
        String s = raw.length() > 200_000 ? raw.substring(0, 200_000) : raw;

        // 1) 위험 블록/잔재 태그 제거(내용째).
        s = DANGEROUS_BLOCKS.matcher(s).replaceAll("");
        s = DANGEROUS_SOLO.matcher(s).replaceAll("");
        // 2) 이벤트 핸들러·위험 스킴 속성 제거.
        s = EVENT_ATTR.matcher(s).replaceAll("");
        s = DANGEROUS_URL.matcher(s).replaceAll("");
        // 3) 화이트리스트에 없는 태그는 태그만 제거(텍스트 보존).
        Matcher m = ANY_TAG.matcher(s);
        StringBuilder out = new StringBuilder(s.length());
        while (m.find()) {
            String tag = m.group(2).toLowerCase();
            m.appendReplacement(out, ALLOWED.contains(tag) ? Matcher.quoteReplacement(m.group()) : "");
        }
        m.appendTail(out);
        return out.toString();
    }
}
