package com.eactive.eai.rms.onl.transaction.apim;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.eactive.eai.data.entity.onl.message.EAIMessageEntity;
import com.eactive.eai.rms.common.base.OnlBaseAnnotationController;
import com.eactive.eai.rms.data.entity.onl.eaimsg.EAIMessageService;
import com.eactive.eai.rms.onl.manage.rule.layout.ui.LayoutUI;
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiInterfaceUI;
import com.eactive.eai.rms.onl.transaction.apim.ui.ApiSpecInfoUI;
import com.eactive.apim.portal.portalproperty.service.PortalPropertyService;
import com.eactive.eai.rms.data.entity.onl.apim.apigroup.ApiGroupService;
import com.eactive.eai.data.entity.onl.apim.apigroup.ApiGroup;
import org.springframework.ui.Model;

/**
 * 신규 OpenAPI Spec 6단계 편집 마법사 컨트롤러.
 *
 * apiInterfaceMan 상세의 "OpenAPI Spec" 버튼이 새 탭(_blank)으로 여는 페이지 및 마법사가 호출하는
 * cmd API 를 제공한다. 저장은 기존 컬럼 + testbedSpec(OpenAPI JSON)에 매핑(스키마 무변경, 경량).
 *
 * (MVC 컨트롤러는 springapp-servlet.xml 의 com.eactive.eai.rms 스캔 범위에서만 등록되므로 본 컨트롤러는
 *  해당 패키지에 둔다. 기존 apiSpecMan.view(v1)와 URL 을 분리해 공존한다.)
 */
@Controller
public class DjbApiSpecController extends OnlBaseAnnotationController {

    @Autowired
    private ApiSpecManService apiSpecManService;

    @Autowired
    private ApiInterfaceService apiInterfaceService;

    @Autowired
    private EAIMessageService eaiMessageService;

    @Autowired
    private DjbApiSpecSwaggerEnricher swaggerEnricher;

    @Autowired
    private DjbApiSpecLayoutMergeService layoutMergeService;

    @Autowired
    private PortalPropertyService portalPropertyService;

    @Autowired
    private ApiGroupService apiGroupService;

    private final ObjectMapper objectMapper = new ObjectMapper();

    // GW base URL 단일 기준 = 포탈 DjbTestbedGatewayProperty 의 djb.gateway.base-url (PTL_PROPERTY 그룹 'Portal').
    // 별도 swagger.gw.address 프로퍼티는 두지 않는다.
    private static final String GW_BASE_URL_KEY = "djb.gateway.base-url";
    private static final String GW_BASE_URL_DEFAULT = "PortalMock";
    // 테스트베드 프록시 사용 여부(포탈 DjbTestbedGatewayProperty 와 동일 키). 마법사에 호출/인증 프록시 경유 안내용.
    private static final String USE_PROXY_KEY = "djb.gateway.use-proxy";
    private static final String TOKEN_USE_PROXY_KEY = "djb.gateway.token-use-proxy";
    // "PortalMock" 은 포탈이 요청 origin 으로 치환하는 sentinel 이라 admin 미리보기용 서버 주소로는 부적합 → 대체값 사용.
    private static final String GW_PREVIEW_FALLBACK = "http://127.0.0.1:39310";

    // ── 진입 ────────────────────────────────────────────────
    @GetMapping(value = "/onl/transaction/apim/djbApiSpecMan.view", params = "cmd=DETAIL")
    public String viewDetail(Model model) {
        // GW base URL (djb.gateway.base-url, 그룹 'Portal') — 없으면 기본값으로 생성. DjbTestbedGatewayProperty 와 동일 키.
        String gw = portalPropertyService.getOrCreateProperty(
                "Portal", GW_BASE_URL_KEY, GW_BASE_URL_DEFAULT, "GW base URL (테스트베드 공통)");
        if (StringUtils.isBlank(gw) || GW_BASE_URL_DEFAULT.equalsIgnoreCase(gw.trim())) {
            gw = GW_PREVIEW_FALLBACK;   // PortalMock → admin 미리보기용 구체 주소로 대체
        }
        model.addAttribute("gwAddress", gw.trim());
        // 테스트베드 프록시(/api/call-api) 사용 여부 — mock/gw 호출·인증 프록시 경유 안내 표기용
        model.addAttribute("useProxy", resolveBoolProperty(USE_PROXY_KEY, true));
        model.addAttribute("tokenUseProxy", resolveBoolProperty(TOKEN_USE_PROXY_KEY, true));
        return "/onl/transaction/apim/djbApiSpecManPopup";
    }

    /** 포탈 그룹 프로퍼티(불리언, 레거시 Y/N 포함)를 읽어 boolean 으로 해석. 미설정/파싱불가 시 default. */
    private boolean resolveBoolProperty(String key, boolean def) {
        String v = portalPropertyService.getOrCreateProperty(
                "Portal", key, def ? "true" : "false",
                "테스트베드 프록시(/api/call-api) 사용 여부(true/false)");
        if (v == null) {
            return def;
        }
        String s = v.trim();
        if (s.equalsIgnoreCase("true") || s.equalsIgnoreCase("Y")) {
            return true;
        }
        if (s.equalsIgnoreCase("false") || s.equalsIgnoreCase("N")) {
            return false;
        }
        return def;
    }

    // API 그룹 선택 팝업(iframe 모달 대상). 그룹 목록 그리드는 apiGroupMan.json?cmd=LIST 재사용.
    @GetMapping(value = "/onl/transaction/apim/djbApiSpecMan.view", params = "cmd=GROUP_POPUP")
    public String viewGroupPopup() {
        return "/onl/transaction/apim/djbApiGroupPopup";
    }

    // ── 조회: 저장 spec 우선 + authtype securityScheme 주입 ──
    @PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=DETAIL_SPEC")
    @ResponseBody
    public ResponseEntity<Map<String, String>> detailSpec(String eaiSvcName, String regen) throws Exception {
        Map<String, String> result = new HashMap<>();
        if (StringUtils.isBlank(eaiSvcName)) {
            result.put("status", "fail");
            result.put("errorMsg", "eaiSvcName 이 없습니다.");
            return ResponseEntity.badRequest().body(result);
        }

        // regen=true 이면 저장본 무시하고 레이아웃 기반으로 강제 재생성 (자동 생성/초기화)
        boolean force = "true".equalsIgnoreCase(regen);
        String spec;
        String source;
        ApiSpecInfoUI saved = force ? null : apiSpecManService.selectDetail(eaiSvcName);
        if (!force && saved != null && StringUtils.isNotBlank(saved.getTestbedSpec())) {
            spec = saved.getTestbedSpec();
            source = "saved";
        } else {
            spec = generateSpec(eaiSvcName);
            source = force ? "regenerated" : "generated";
        }

        String authtype = eaiMessageService.findById(eaiSvcName)
                .map(EAIMessageEntity::getAuthtype)
                .orElse(null);
        String enriched = swaggerEnricher.enrich(spec, authtype);

        result.put("testbedSpec", enriched);
        result.put("source", source);
        result.put("authType", authtype == null ? "" : authtype);
        // API 그룹 소속(조인테이블 ApiGroupApi) — 저장 여부와 무관하게 현재 소속 반환
        try {
            ArrayNode groups = objectMapper.createArrayNode();
            for (ApiGroup g : apiGroupService.findByApiId(eaiSvcName)) {
                ObjectNode gn = objectMapper.createObjectNode();
                gn.put("id", g.getId());
                gn.put("groupName", g.getGroupName());
                groups.add(gn);
            }
            result.put("apiGroups", objectMapper.writeValueAsString(groups));
        } catch (Exception e) {
            logger.warn("API 그룹 조회 실패: " + eaiSvcName, e);
        }
        // 저장본이면 폼 복원용 기본정보/공개설정도 함께 반환
        if (saved != null) {
            putIfNotNull(result, "apiName", saved.getApiName());
            putIfNotNull(result, "apiSimpleDescription", saved.getApiSimpleDescription());
            putIfNotNull(result, "apiUrl", saved.getApiUrl());
            putIfNotNull(result, "apiMethod", saved.getApiMethod());
            putIfNotNull(result, "apiContentType", saved.getApiContentType());
            putIfNotNull(result, "description", saved.getDescription());
            putIfNotNull(result, "apiRequestSpec", saved.getApiRequestSpec());
            putIfNotNull(result, "apiResponseSpec", saved.getApiResponseSpec());
            putIfNotNull(result, "sampleRequest", saved.getSampleRequest());
            putIfNotNull(result, "sampleResponse", saved.getSampleResponse());
            putIfNotNull(result, "responseType", saved.getResponseType());
            putIfNotNull(result, "mockUrl", saved.getMockUrl());
            putIfNotNull(result, "displayYn", saved.getDisplayYn());
            putIfNotNull(result, "displayRoleCode", saved.getDisplayRoleCode());
            putIfNotNull(result, "displayOrg", saved.getDisplayOrg());
        }
        // 예제/설명자료(요청·응답 메시지 스펙 HTML)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치.
        // 저장본이면 위 saved 블록에서 저장값을 이미 반환.
        return ResponseEntity.ok(result);
    }

    // ── 상태조회: apiInterfaceMan 상세의 OpenAPI Spec 버튼 색상용 ──
    // PTL_API_SPEC_INFO(api_id = eaiSvcName) 존재 여부와 display_yn 만 경량 반환.
    @PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=SPEC_STATUS")
    @ResponseBody
    public ResponseEntity<Map<String, String>> specStatus(String eaiSvcName) {
        Map<String, String> result = new HashMap<>();
        boolean exists = false;
        String displayYn = "N";
        if (StringUtils.isNotBlank(eaiSvcName)) {
            // selectDetail 은 미존재 시에도 빈 ApiSpecInfoUI(apiId=null)를 반환하므로 apiId 로 존재 판정.
            ApiSpecInfoUI saved = apiSpecManService.selectDetail(eaiSvcName);
            if (saved != null && StringUtils.isNotBlank(saved.getApiId())) {
                exists = true;
                displayYn = StringUtils.defaultIfBlank(saved.getDisplayYn(), "N");
            }
        }
        result.put("exists", String.valueOf(exists));
        result.put("displayYn", displayYn);
        return ResponseEntity.ok(result);
    }

    // ── 저장: 마법사 폼 → ApiSpecInfoUI → apiSpecManService.save (기존 재사용) ──
    @PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=SAVE")
    @ResponseBody
    public ResponseEntity<Map<String, String>> save(ApiSpecInfoUI apiSpecInfoUI, String apiGroupIds) {
        Map<String, String> result = new HashMap<>();
        if (apiSpecInfoUI == null || StringUtils.isBlank(apiSpecInfoUI.getApiId())) {
            result.put("status", "fail");
            result.put("errorMsg", "API ID 가 없습니다.");
            return ResponseEntity.badRequest().body(result);
        }
        // CrossScriptingFilter(RequestWrapper.cleanXSS)가 요청 파라미터의 특수문자를 엔티티로 치환
        // ('('→&#40; ')'→&#41; '"'→&quot; 등)하므로, 저장 전 모든 자유텍스트/JSON 필드를 언이스케이프해 원복한다.
        // (JSON 필드는 특히 '"'→&quot; 로 깨지므로 필수)
        apiSpecInfoUI.setApiName(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiName()));
        apiSpecInfoUI.setApiSimpleDescription(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiSimpleDescription()));
        apiSpecInfoUI.setApiUrl(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiUrl()));
        apiSpecInfoUI.setMockUrl(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getMockUrl()));
        apiSpecInfoUI.setApiRequestSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiRequestSpec()));
        apiSpecInfoUI.setApiResponseSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getApiResponseSpec()));
        apiSpecInfoUI.setSampleRequest(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getSampleRequest()));
        apiSpecInfoUI.setSampleResponse(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getSampleResponse()));
        apiSpecInfoUI.setDescription(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getDescription()));
        apiSpecInfoUI.setTestbedSpec(StringEscapeUtils.unescapeHtml(apiSpecInfoUI.getTestbedSpec()));

        apiSpecManService.save(apiSpecInfoUI);

        // API 그룹 소속(조인테이블) 동기화 — apiGroupIds(CSV) 로 재설정
        try {
            java.util.List<String> gids = new ArrayList<>();
            if (StringUtils.isNotBlank(apiGroupIds)) {
                for (String g : apiGroupIds.split(",")) {
                    if (StringUtils.isNotBlank(g)) {
                        gids.add(g.trim());
                    }
                }
            }
            apiGroupService.setApiGroupsForApi(apiSpecInfoUI.getApiId(), gids);
        } catch (Exception e) {
            logger.warn("API 그룹 동기화 실패: " + apiSpecInfoUI.getApiId(), e);
        }

        result.put("status", "success");
        result.put("apiId", apiSpecInfoUI.getApiId());
        return ResponseEntity.ok(result);
    }

    // ── 레이아웃 불러오기 (GW) ──
    @PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=LOAD_REQUEST_LAYOUT")
    @ResponseBody
    public ResponseEntity<Map<String, Object>> loadRequestLayout(String eaiSvcName) throws Exception {
        return loadLayout(eaiSvcName, true);
    }

    @PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=LOAD_RESPONSE_LAYOUT")
    @ResponseBody
    public ResponseEntity<Map<String, Object>> loadResponseLayout(String eaiSvcName) throws Exception {
        return loadLayout(eaiSvcName, false);
    }

    // ── HTML 테이블 생성 (기존 generateSpecTableFromLayout 재사용) ──
    @PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=GENERATE_HTML")
    @ResponseBody
    public ResponseEntity<Map<String, String>> generateHtml(String eaiSvcName, String layoutType) throws Exception {
        LayoutUI layout = resolveLayout(eaiSvcName, !"RESPONSE".equalsIgnoreCase(layoutType));
        Map<String, String> result = new HashMap<>();
        result.put("html", apiSpecManService.generateSpecTableFromLayout(layout));
        return ResponseEntity.ok(result);
    }

    // ── 메시지 예제 생성 (기존 generateSampleDataFromLayout 재사용) ──
    @PostMapping(value = "/onl/transaction/apim/djbApiSpecMan.json", params = "cmd=GENERATE_SAMPLE")
    @ResponseBody
    public ResponseEntity<Map<String, String>> generateSample(String eaiSvcName, String layoutType) throws Exception {
        LayoutUI layout = resolveLayout(eaiSvcName, !"RESPONSE".equalsIgnoreCase(layoutType));
        Map<String, String> result = new HashMap<>();
        result.put("json", apiSpecManService.generateSampleDataFromLayout(layout));
        return ResponseEntity.ok(result);
    }

    // ── 내부 헬퍼 ──────────────────────────────────────────
    private ResponseEntity<Map<String, Object>> loadLayout(String eaiSvcName, boolean request) throws Exception {
        Map<String, Object> res = new LinkedHashMap<>();
        List<String> warnings = new ArrayList<>();
        if (StringUtils.isBlank(eaiSvcName)) {
            res.put("layoutType", request ? "REQUEST" : "RESPONSE");
            res.put("fields", new ArrayList<>());
            warnings.add("eaiSvcName 이 없습니다.");
            res.put("warnings", warnings);
            return ResponseEntity.ok(res);
        }
        LayoutUI layout = resolveLayout(eaiSvcName, request);
        if (layout == null || layout.getLayoutItems() == null || layout.getLayoutItems().isEmpty()) {
            warnings.add((request ? "요청" : "응답") + " 레이아웃이 없습니다.");
        }
        res.put("layoutType", request ? "REQUEST" : "RESPONSE");
        res.put("fields", layoutMergeService.toFields(layout, request ? "REQUEST" : "RESPONSE"));
        res.put("warnings", warnings);
        return ResponseEntity.ok(res);
    }

    private LayoutUI resolveLayout(String eaiSvcName, boolean request) throws Exception {
        ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
        if (apiInterfaceUI == null) {
            return null;
        }
        String layoutName = request ? apiInterfaceUI.getInboundRequestLayout() : apiInterfaceUI.getInboundResponseLayout();
        return StringUtils.isNotEmpty(layoutName) ? apiSpecManService.selectLayoutUI(layoutName) : null;
    }

    /**
     * 레이아웃 기반 자동 생성 + 자동생성 규칙(백엔드 단일 소스).
     * 규칙: title/tag/summary = API명(eaiSvcDesc), operationId = 인터페이스ID,
     *      servers[0].url 을 path 앞에 붙이고 servers 제거(서버 선택 제거),
     *      요청/200 응답 예제를 레이아웃 샘플로 임베드.
     */
    private String generateSpec(String eaiSvcName) throws Exception {
        ApiInterfaceUI apiInterfaceUI = apiInterfaceService.selectDetail(eaiSvcName);
        if (apiInterfaceUI == null) {
            throw new IllegalStateException("API 인터페이스 정보를 찾을 수 없습니다: " + eaiSvcName);
        }
        Map<String, String> inboundAdapterSpec = apiInterfaceService.getHttpAdapterInfo(apiInterfaceUI.getFromAdapter());
        if (inboundAdapterSpec == null) {
            inboundAdapterSpec = new HashMap<>();
        }
        String requestLayoutName = apiInterfaceUI.getInboundRequestLayout();
        String responseLayoutName = apiInterfaceUI.getInboundResponseLayout();
        LayoutUI requestLayoutUI = StringUtils.isNotEmpty(requestLayoutName)
                ? apiSpecManService.selectLayoutUI(requestLayoutName) : null;
        LayoutUI responseLayoutUI = StringUtils.isNotEmpty(responseLayoutName)
                ? apiSpecManService.selectLayoutUI(responseLayoutName) : null;

        String baseSpec = apiSpecManService.generateSwaggerSpec(requestLayoutUI, responseLayoutUI, inboundAdapterSpec, apiInterfaceUI);

        String apiName = StringUtils.isNotBlank(apiInterfaceUI.getEaiSvcDesc()) ? apiInterfaceUI.getEaiSvcDesc() : eaiSvcName;
        String reqSample = apiSpecManService.generateSampleDataFromLayout(requestLayoutUI);
        String resSample = apiSpecManService.generateSampleDataFromLayout(responseLayoutUI);
        String contentType = StringUtils.isNotBlank(apiInterfaceUI.getRestContentType()) ? apiInterfaceUI.getRestContentType() : "application/json";
        return applyAutoRules(baseSpec, apiName, eaiSvcName, reqSample, resSample, contentType);
    }

    /** 자동생성 규칙 후처리(Jackson 트리 조작). 실패 시 원본 유지. */
    private String applyAutoRules(String specJson, String apiName, String eaiSvcName, String reqSample, String resSample, String contentType) {
        try {
            JsonNode parsed = objectMapper.readTree(specJson);
            if (!parsed.isObject()) {
                return specJson;
            }
            ObjectNode root = (ObjectNode) parsed;

            root.put("openapi", "3.1.0");   // OpenAPI 3.1 방출(Swagger UI 5.x oas31 렌더 지원)
            getOrCreateObject(root, "info").put("title", apiName);

            String tagName = "DJBank";   // 자동생성 태그명 고정
            ArrayNode tags = objectMapper.createArrayNode();
            ObjectNode tag = objectMapper.createObjectNode();
            tag.put("name", tagName);
            tag.put("description", apiName);   // 태그 설명 = 간단설명(API명)
            tags.add(tag);
            root.set("tags", tags);

            // 어댑터경로(servers[0].url = getHttpAdapterInfo urlPath)를 path 앞에 baking 하고 servers 제거.
            // 호스트(gw=djb.gateway.base-url / mock=Mock URL / sample=없음)는 프론트가 응답유형에 따라 서버로 붙인다.
            String adapterPath = "";
            JsonNode serversNode = root.get("servers");
            if (serversNode != null && serversNode.isArray() && serversNode.size() > 0 && serversNode.get(0).get("url") != null) {
                adapterPath = serversNode.get(0).get("url").asText("");
            }
            root.remove("servers");

            JsonNode pathsNode = root.get("paths");
            if (pathsNode != null && pathsNode.isObject()) {
                ObjectNode paths = (ObjectNode) pathsNode;
                List<String> pathKeys = new ArrayList<>();
                Iterator<String> pit = paths.fieldNames();
                while (pit.hasNext()) {
                    pathKeys.add(pit.next());
                }
                for (String pk : pathKeys) {
                    JsonNode pathItem = paths.get(pk);
                    if (pathItem != null && pathItem.isObject()) {
                        List<String> methods = new ArrayList<>();
                        Iterator<String> mit = pathItem.fieldNames();
                        while (mit.hasNext()) {
                            methods.add(mit.next());
                        }
                        for (String mk : methods) {
                            JsonNode opNode = pathItem.get(mk);
                            if (opNode != null && opNode.isObject()) {
                                ObjectNode op = (ObjectNode) opNode;
                                op.put("operationId", eaiSvcName);
                                op.put("summary", apiName);
                                ArrayNode opTags = objectMapper.createArrayNode();
                                opTags.add(tagName);
                                op.set("tags", opTags);
                                // 예제(요청/응답 본문)는 프론트에서 스키마 기준으로 생성 → 자동생성/재생성 결과 일치. 여기선 임베드하지 않음.
                                markGwOnOperation(op);
                                addResponseHeader(op, "200", "Content-Type", contentType);
                            }
                        }
                    }
                    String newKey = adapterPath.isEmpty() ? pk
                            : (adapterPath.replaceAll("/+$", "") + (pk.startsWith("/") ? "" : "/") + pk);
                    if (!newKey.equals(pk)) {
                        paths.set(newKey, pathItem);
                        paths.remove(pk);
                    }
                }
            }
            return objectMapper.writeValueAsString(root);
        } catch (Exception e) {
            logger.error("applyAutoRules 실패, 원본 spec 유지", e);
            return specJson;
        }
    }

    private void setContentExample(ObjectNode op, String sampleJson) {
        if (StringUtils.isBlank(sampleJson)) {
            return;
        }
        JsonNode body = op.get("requestBody");
        if (body == null || !body.isObject()) {
            return;
        }
        JsonNode content = body.get("content");
        if (content == null || !content.isObject()) {
            return;
        }
        Iterator<String> mts = content.fieldNames();
        if (!mts.hasNext()) {
            return;
        }
        JsonNode mtNode = content.get(mts.next());
        if (mtNode != null && mtNode.isObject()) {
            ((ObjectNode) mtNode).set("example", parseJsonOrText(sampleJson));
        }
    }

    private void setResponseExample(ObjectNode op, String code, String sampleJson) {
        if (StringUtils.isBlank(sampleJson)) {
            return;
        }
        JsonNode resps = op.get("responses");
        if (resps == null || !resps.isObject()) {
            return;
        }
        JsonNode r = resps.get(code);
        if (r == null || !r.isObject()) {
            return;
        }
        ObjectNode content = getOrCreateObject((ObjectNode) r, "content");
        ObjectNode appjson = getOrCreateObject(content, "application/json");
        appjson.set("example", parseJsonOrText(sampleJson));
    }

    /** GW 유래 스키마 필드에 x-djb-gw 마커 부여(프론트에서 잠금 표시). */
    private void markGwOnOperation(ObjectNode op) {
        JsonNode body = op.get("requestBody");
        if (body != null && body.isObject()) {
            markGwInContent(body.get("content"));
        }
        JsonNode resps = op.get("responses");
        if (resps != null && resps.isObject()) {
            List<String> codes = new ArrayList<>();
            Iterator<String> it = resps.fieldNames();
            while (it.hasNext()) {
                codes.add(it.next());
            }
            for (String c : codes) {
                JsonNode r = resps.get(c);
                if (r != null && r.isObject()) {
                    markGwInContent(r.get("content"));
                }
            }
        }
    }

    private void markGwInContent(JsonNode content) {
        if (content == null || !content.isObject()) {
            return;
        }
        List<String> mts = new ArrayList<>();
        Iterator<String> it = content.fieldNames();
        while (it.hasNext()) {
            mts.add(it.next());
        }
        for (String mt : mts) {
            JsonNode m = content.get(mt);
            if (m != null && m.isObject()) {
                markGwSchema(m.get("schema"));
            }
        }
    }

    private void markGwSchema(JsonNode schema) {
        if (schema == null || !schema.isObject()) {
            return;
        }
        ObjectNode s = (ObjectNode) schema;
        JsonNode props = s.get("properties");
        if (props != null && props.isObject()) {
            ArrayNode required = (s.get("required") != null && s.get("required").isArray())
                    ? (ArrayNode) s.get("required") : objectMapper.createArrayNode();
            java.util.Set<String> existing = new java.util.HashSet<>();
            required.forEach(n -> existing.add(n.asText()));
            List<String> keys = new ArrayList<>();
            Iterator<String> it = props.fieldNames();
            while (it.hasNext()) {
                keys.add(it.next());
            }
            for (String k : keys) {
                JsonNode p = props.get(k);
                if (p != null && p.isObject()) {
                    ((ObjectNode) p).put("x-djb-gw", true);
                    if (!existing.contains(k)) {   // GW 필드는 기본 '필수'
                        required.add(k);
                        existing.add(k);
                    }
                    markGwSchema(p);
                }
            }
            if (required.size() > 0) {
                s.set("required", required);
            }
        }
        JsonNode items = s.get("items");
        if (items != null && items.isObject()) {
            markGwSchema(items);
        }
    }

    /** 응답 헤더 미리 등록(중복 시 스킵). */
    private void addResponseHeader(ObjectNode op, String code, String headerName, String example) {
        JsonNode resps = op.get("responses");
        if (resps == null || !resps.isObject()) {
            return;
        }
        JsonNode r = resps.get(code);
        if (r == null || !r.isObject()) {
            return;
        }
        ObjectNode headers = getOrCreateObject((ObjectNode) r, "headers");
        if (headers.has(headerName)) {
            return;
        }
        ObjectNode h = objectMapper.createObjectNode();
        h.put("description", headerName);
        ObjectNode sc = objectMapper.createObjectNode();
        sc.put("type", "string");
        h.set("schema", sc);
        h.put("example", example);
        headers.set(headerName, h);
    }

    private JsonNode parseJsonOrText(String s) {
        try {
            return objectMapper.readTree(s);
        } catch (Exception e) {
            return TextNode.valueOf(s);
        }
    }

    private ObjectNode getOrCreateObject(ObjectNode parent, String field) {
        JsonNode node = parent.get(field);
        if (node != null && node.isObject()) {
            return (ObjectNode) node;
        }
        ObjectNode created = objectMapper.createObjectNode();
        parent.set(field, created);
        return created;
    }

    private void putIfNotNull(Map<String, String> map, String key, String value) {
        if (value != null) {
            map.put(key, value);
        }
    }
}
