"""OpenAI structured output strict mode 호환성 — 재귀 lint.

사용자 진단 (2026-05-08, t2i_review scene_index runtime fail):
> prompt schema 변경이 provider strict 계약까지 검증되지 않고, mocked
> integration 만 통과해서 runtime 에서 터지는 구조적 결함

OpenAI strict mode 규칙:
  > 'required' is required to be supplied and to be an array including every key
  > in properties.

즉 모든 nested object 의 `properties` 에 정의된 모든 key 는 `required` 배열에
포함되어야 한다. optional 의미가 필요하면 `type` 을 nullable list (예: ["integer",
"null"]) 로 표현 — required 자리에는 그대로 두되 LLM 이 null 반환 가능.

본 lint 는 prompts/_base/ 아래 모든 *_schema.json 을 재귀 walk 해서 위반을 잡는다.
한 번 박혀 있던 위반:
- prompts/_base/t2i_review/2.202605081200/scene_schema.json — scene_index/var_index
  가 properties 에 있지만 required 에 없어 OpenAI 가 모든 scene batch reject.
"""
import json
from pathlib import Path
from typing import List

import app.modules.prompt_loader as prompt_loader


def _walk_strict_violations(
    node, path: str, *, require_no_additional_properties: bool = False
) -> List[str]:
    """schema 트리를 walk 하며 strict mode 위반 수집.

    OpenAI structured outputs strict mode 규칙:
    1. `type: object` + `properties` 인 노드: 모든 properties key 가 required 에 포함.
    2. (opt-in) `type: object` 인 모든 노드: `additionalProperties: false` 명시 의무.
       자유 object (`{"type": "object"}` without properties) 는 strict reject —
       명시 nested schema 또는 anyOf null pattern 사용.
       `require_no_additional_properties=True` 시 활성화. prompts/_base 의 historical
       schema 보호 위해 default False — 신규 code-defined schema lint 에서만 True.

    nullable 의도: `type=["x", "null"]` (primitive) 또는 anyOf with `{"type": "null"}` (object).

    Args:
        node: schema 의 sub-tree (dict 또는 list 또는 primitive).
        path: 현재 위치 (디버깅용 path string, 예: "$.properties.results.items").
        require_no_additional_properties: True 면 모든 type:object 에 additionalProperties:false
            의무 (Rule 2 활성화).

    Returns:
        violation 메시지 list. 빈 list 면 strict 호환.
    """
    violations: List[str] = []
    if isinstance(node, dict):
        # object 노드: properties 와 required + additionalProperties 검사
        if node.get("type") == "object":
            # Rule 2 (opt-in): additionalProperties: false 의무.
            if require_no_additional_properties:
                if "additionalProperties" not in node or node["additionalProperties"] is not False:
                    violations.append(
                        f"{path}: type=object 에 additionalProperties: false 누락. "
                        f"OpenAI strict 위반 (자유 object reject)."
                    )
            # Rule 1: properties keys ⊆ required (해당 properties 정의 시).
            if "properties" in node:
                properties = node.get("properties") or {}
                required = set(node.get("required") or [])
                missing = set(properties.keys()) - required
                if missing:
                    violations.append(
                        f"{path}: properties keys {sorted(missing)} 가 required 에 없음. "
                        f"OpenAI strict 위반. nullable 의도면 type=[\"x\",\"null\"] 또는 "
                        f"anyOf-null + required 에 포함 패턴 사용."
                    )
        # 재귀 — 모든 child 도 검사 (items / properties / oneOf / anyOf 등)
        for k, v in node.items():
            violations.extend(_walk_strict_violations(
                v, f"{path}.{k}",
                require_no_additional_properties=require_no_additional_properties,
            ))
    elif isinstance(node, list):
        for i, v in enumerate(node):
            violations.extend(_walk_strict_violations(
                v, f"{path}[{i}]",
                require_no_additional_properties=require_no_additional_properties,
            ))
    return violations


def _latest_schema_files() -> List[Path]:
    """각 step_id 의 latest version directory 의 *_schema.json 만 수집.

    옛 historical version 은 archival — 운영에서 자동 선택 안 됨. lint 는 active
    prompt pack 만 검사 (사용자 정책: 2026-05-08).

    Layout: prompts/_base/{step_id}/{version}/{*_schema.json}
    """
    base = prompt_loader.PROMPTS_BASE
    schemas: List[Path] = []
    for step_dir in sorted(p for p in base.iterdir() if p.is_dir()):
        version_dirs = [d for d in step_dir.iterdir() if d.is_dir()]
        if not version_dirs:
            continue
        latest = sorted(
            version_dirs,
            key=lambda d: prompt_loader._version_sort_key(d.name),
            reverse=True,
        )[0]
        schemas.extend(sorted(latest.glob("*_schema.json")))
    return schemas


def test_latest_prompt_schemas_openai_strict_compatible():
    """각 step_id 의 latest prompt schema 가 OpenAI strict mode 호환.

    사용자 진단 회귀 가드 (2026-05-08): schema 가 properties 에 정의한 key 는 모두
    required 에도 있어야 한다. optional 의미가 필요하면 nullable type
    (e.g. ["integer", "null"]) 으로 표현하거나 빈 배열 [] 로 표현하고 required 에는
    포함. 옛 historical version 은 archival — lint 검사 외.
    """
    schemas = _latest_schema_files()
    assert schemas, "prompts/_base/ 아래 latest *_schema.json 이 0건 — 검색 path 문제"

    all_violations: List[str] = []
    for schema_path in schemas:
        try:
            data = json.loads(schema_path.read_text(encoding="utf-8"))
        except json.JSONDecodeError as exc:
            all_violations.append(f"{schema_path}: invalid JSON ({exc})")
            continue
        violations = _walk_strict_violations(data, "$")
        for v in violations:
            all_violations.append(f"{schema_path}: {v}")

    assert not all_violations, (
        f"OpenAI strict mode 위반 {len(all_violations)} 건:\n"
        + "\n".join(f"  - {v}" for v in all_violations)
    )


# ─────────────────────────────────────────────────────────────────────
# code-defined schema strict lint — D6 T2-fix2 (review iter4 I1)
# ─────────────────────────────────────────────────────────────────────

# code-defined schema 는 prompts/_base 와 별개로 관리. 추가 시 본 list 에 등록 +
# Rule 1 + Rule 2 (additionalProperties:false 의무) 강제.
_CODE_DEFINED_SCHEMAS = [
    ("app.modules.pipeline.entity_extractor_v3", "ENTITY_DETAIL_SCHEMA"),
]


def test_code_defined_schemas_openai_strict_compatible():
    """code-defined schema (예: ENTITY_DETAIL_SCHEMA) OpenAI strict 호환.

    review iter4 I1: ENTITY_DETAIL_SCHEMA 의 metadata_json 이 자유 object 였음
    (free `{"type": "object"}`) — strict mode reject 위험. anyOf null pattern 으로
    명시 nested schema 강제. prompts/_base lint 와 별개로 더 엄격한 Rule 2 적용.

    추가 strict-required code schema 는 `_CODE_DEFINED_SCHEMAS` 에 등록.
    """
    import importlib

    all_violations: List[str] = []
    for module_path, attr_name in _CODE_DEFINED_SCHEMAS:
        mod = importlib.import_module(module_path)
        schema = getattr(mod, attr_name)
        violations = _walk_strict_violations(
            schema, f"${{{module_path}.{attr_name}}}",
            require_no_additional_properties=True,
        )
        all_violations.extend(violations)

    assert not all_violations, (
        f"code-defined schema OpenAI strict 위반 {len(all_violations)} 건:\n"
        + "\n".join(f"  - {v}" for v in all_violations)
    )
