"""
이미지 제작 QC 게이트 워크플로우 (image_workflow.py)

4+1 Phase 이미지 제작 워크플로우의 프롬프트 생성 및 QC 기준을 정의.
dispatch.py에서 --workflow image-qc-gate 옵션으로 호출 가능하도록 설계.

Usage:
    from prompts.image_workflow import build_image_workflow_prompt, QC_CRITERIA
"""

import sys
from pathlib import Path

# config/dq_rules 참조를 위한 경로 설정
_WORKSPACE_DIR = Path(__file__).resolve().parent.parent
if str(_WORKSPACE_DIR) not in sys.path:
    sys.path.insert(0, str(_WORKSPACE_DIR))

from config.loader import ConfigManager  # pyright: ignore[reportMissingImports]
from tools.dq_rules import ABSOLUTE_MIN_PX, DQ_RULES, PASS_THRESHOLD  # pyright: ignore[reportMissingImports]

_cfg = ConfigManager.get_instance()
WORKSPACE_ROOT = _cfg.get_path("roots.workspace")

# DQ 규칙 파생 상수 (dq-rules.json 단일 소스 참조)
_HEADLINE_MIN = DQ_RULES["font_sizes"]["headline"]["min"]  # 84
_SUBHEAD_MIN = DQ_RULES["font_sizes"]["subhead"]["min"]  # 64
_BANNED_WEIGHTS = DQ_RULES["font_weights"]["banned_weights"]  # [100, 200, 300]
_MAX_BANNED_WEIGHT = max(_BANNED_WEIGHTS)  # 300
_MIN_ALLOWED_WEIGHT = _MAX_BANNED_WEIGHT + 100  # 400 (CSS font-weight는 100 단위)

# QC 카테고리 A (자동 검증 가능) - 전항목 PASS 필수
QC_CATEGORY_A: dict[str, dict[str, str]] = {
    "A-01": {"name": "글자 겹침", "check": "텍스트 요소 간 경계 겹침 없음", "type": "auto"},
    "A-02": {"name": "경계 이탈", "check": "모든 텍스트/요소가 안전 영역 내 존재", "type": "auto"},
    "A-03": {"name": "해상도 규격", "check": "출력 해상도가 플랫폼 요구사항 충족", "type": "auto"},
    "A-04": {
        "name": "폰트 규칙",
        "check": f"헤드라인 {_HEADLINE_MIN}px/서브 {_SUBHEAD_MIN}px/바디 {ABSOLUTE_MIN_PX}px 준수",
        "type": "auto",
    },
    "A-05": {"name": "satori 사용 여부", "check": "satori-cardnews 스킬 미사용 확인", "type": "auto"},
    "A-06": {
        "name": "텍스트 오버플로우",
        "check": "텍스트가 Zone 경계를 벗어나지 않음 (scrollWidth <= clientWidth)",
        "type": "auto",
    },
    "A-07": {
        "name": "텍스트-배경 대비율",
        "check": "worst-case pixel 기준 WCAG AAA 7:1 (대형텍스트 4.5:1) 이상",
        "type": "auto",
    },
    "A-08": {
        "name": "폰트 크기 최소",
        "check": f"모든 텍스트 요소(페이지 인디케이터, 브랜드 태그, 면책 문구 포함) 최소 {ABSOLUTE_MIN_PX}px 이상 (예외 없음)",
        "type": "auto",
    },
    "A-09": {
        "name": "폰트 두께",
        "check": f"font-weight {_MAX_BANNED_WEIGHT} 이하(light/thin) 사용 금지, 최소 {_MIN_ALLOWED_WEIGHT}(regular) 이상",
        "type": "auto",
    },
}

# QC 카테고리 B (수동 검증) - 70% 이상 PASS
QC_CATEGORY_B: dict[str, dict[str, float | str]] = {
    "B-01": {"name": "CTA 명확성", "weight_conversion": 0.30, "weight_brand": 0.10},
    "B-02": {"name": "메시지 일관성", "weight_conversion": 0.25, "weight_brand": 0.25},
    "B-03": {"name": "브랜드 정합성", "weight_conversion": 0.20, "weight_brand": 0.20},
    "B-04": {"name": "시각적 계층 구조", "weight_conversion": 0.15, "weight_brand": 0.25},
    "B-05": {"name": "감정적 공감도", "weight_conversion": 0.10, "weight_brand": 0.20},
}

# FAIL 사유 카테고리 (enum 13종)
FAIL_CATEGORIES: list[str] = [
    "TEXT_OVERLAP",  # 텍스트 겹침
    "BOUNDARY_VIOLATION",  # 경계 이탈
    "RESOLUTION_FAIL",  # 해상도 미달
    "CTA_MISSING",  # CTA 부재/불명확
    "MESSAGE_UNCLEAR",  # 메시지 불명확
    "EMOTION_FLAT",  # 감정 공감 부족
    "HOOK_WEAK",  # 후킹력 부족
    "BRAND_INCONSISTENT",  # 브랜드 불일치
    "COPY_REDUNDANT",  # 카피 중복/불일치
    "TEXT_OVERFLOW",  # 텍스트 오버플로우 (Zone 이탈)
    "CONTRAST_FAIL",  # 대비율 미달
    "FONT_SIZE_FAIL",  # 폰트 크기 미달
    "FONT_WEIGHT_FAIL",  # 폰트 두께 미달
]

# 허용 폰트 정책 (B+D 템플릿 시스템)
# 폰트 정책: 제한 없음 (제이회장님 지시 2026-03-29 — "폰트도 디자인, 다양해야 함")
# Canva 유료 독점 폰트만 제외 (HTML 렌더링 불가), 나머지 자유 선택
FONT_POLICY_NOTE: str = (
    "폰트 선택은 디자인의 일부이므로 종류 제한 없음. "
    "단, Canva 유료 독점 폰트(HTML 렌더링 불가)만 제외. "
    "Google Fonts, 오픈소스(SIL OFL) 폰트 자유 사용 가능."
)

# 파이프라인 완료 토큰 (잠금 장치)
PIPELINE_TOKENS: list[str] = [
    "KNOWHOW_PRELOAD_OK",  # Phase -1 노하우 프리로딩 완료
    "BRIEF_QC_OK",  # Phase 0.5 마아트 발급
    "COPY_DRAFT_OK",  # Phase 1 자동 발급 (Phase 0.5 필수)
    "PLAN_QC_OK",  # Phase 1.5 마아트+로키 발급
    "PILOT_OK",  # Phase 2.5 아마테라스+로키 발급
    "AUTO_QC_OK",  # Phase 3 자동 발급
    "DESIGN_QC_OK",  # Phase 3.5 로키 단독 발급
    "MANUAL_QC_OK",  # Phase 4 3인 서명 발급
]

# 에스컬레이션 조건
ESCALATION_RULES: dict[str, int] = {
    "copy_loop_max": 5,  # 카피 QC 루프 상한
    "design_loop_max": 3,  # 디자인 QC 루프 상한
    "same_fail_repeat": 2,  # 동일 FAIL 카테고리 연속 반복 시
    "plan_qc_immediate_pass": 90,  # 기획 QC: 90점↑ = 즉시 PASS
    "design_qc_immediate_pass": 97,  # 디자인 QC: 97점↑ = 즉시 PASS
}

# QC 기준 통합 (외부 노출용)
QC_CRITERIA: dict[str, object] = {
    "category_a": QC_CATEGORY_A,
    "category_b": QC_CATEGORY_B,
    "fail_categories": FAIL_CATEGORIES,
    "escalation_rules": ESCALATION_RULES,
}

KNOWHOW_PATH = f"{WORKSPACE_ROOT}/memory/specs/design-qc-knowhow.md"
KNOWHOW_MARKETING_PATH = f"{WORKSPACE_ROOT}/memory/specs/knowhow-marketing.md"
KNOWHOW_DESIGN_PATH = f"{WORKSPACE_ROOT}/memory/specs/knowhow-design.md"


def _build_category_a_section() -> str:
    """QC 카테고리 A 섹션 문자열 생성"""
    lines = "### QC 카테고리 A (자동 검증) — 전항목 PASS 필수\n"
    for code, item in QC_CATEGORY_A.items():
        lines += f"- [{code}] {item['name']}: {item['check']}\n"
    return lines


def _build_category_b_section(campaign_type: str = "conversion") -> str:
    """QC 카테고리 B 섹션 문자열 생성 (프로파일별 가중치 적용)"""
    weight_key = f"weight_{campaign_type}" if campaign_type in ("conversion", "brand") else "weight_conversion"
    lines = f"### QC 카테고리 B (수동 검증) — 가중 합산 70점 이상 PASS (프로파일: {campaign_type})\n"
    for code, item in QC_CATEGORY_B.items():
        weight_val = item.get(weight_key, item["weight_conversion"])
        weight_pct = int(float(str(weight_val)) * 100)
        lines += f"- [{code}] {item['name']}: 가중치 {weight_pct}%\n"
    lines += (
        "\n**가중 점수 계산 방법:**\n"
        "각 항목을 0~100점으로 평가 후, 가중치를 곱하여 합산.\n"
        "합산 점수 ≥ 70 → PASS / < 70 → FAIL\n"
    )
    return lines


def _build_fail_categories_section() -> str:
    """FAIL 사유 카테고리 섹션 문자열 생성"""
    lines = "### FAIL 사유 카테고리 (13종)\n"
    fail_desc = {
        "TEXT_OVERLAP": "글자 겹침",
        "BOUNDARY_VIOLATION": "경계 이탈",
        "RESOLUTION_FAIL": "해상도 미달",
        "FONT_RULE_VIOLATION": "폰트 규칙 위반",
        "SATORI_USAGE": "satori 사용",
        "MESSAGE_UNCLEAR": "메시지 불명확",
        "CTA_WEAK": "CTA 약함",
        "BRAND_MISMATCH": "브랜드 불일치",
        "LAYOUT_IMBALANCE": "레이아웃 불균형",
        "TEXT_OVERFLOW": "텍스트 오버플로우 (Zone 이탈)",
        "CONTRAST_FAIL": "대비율 미달",
        "FONT_SIZE_FAIL": "폰트 크기 미달",
        "FONT_WEIGHT_FAIL": f"폰트 두께 미달 (weight {_MAX_BANNED_WEIGHT} 이하)",
    }
    for code, desc in fail_desc.items():
        lines += f"- {code}: {desc}\n"
    lines += "⚠️ FAIL 기록 시 반드시 위 13종 중 하나를 정확히 명시하세요.\n"
    return lines


def build_phase_minus1_prompt(task_id: str, phase_group: str = "final") -> str:
    """Phase -1: 노하우 프리로딩 (매 위임 묶음마다 반복)

    담당: 복합팀 팀장 (Opus)
    필독 파일을 읽고 핵심 요약을 보고서에 기재해야 Phase 0 토큰 발급.

    Args:
        task_id: 작업 ID
        phase_group: 로딩할 노하우 파일 그룹
            - "copy": knowhow-marketing.md + design-qc-knowhow.md (2개)
            - "design": knowhow-design.md + design-qc-knowhow.md (2개)
            - "final": 3개 모두 (기본값)
    """
    if phase_group not in ("copy", "design", "final"):
        raise ValueError(f"유효하지 않은 phase_group: '{phase_group}'. 허용값: 'copy', 'design', 'final'")

    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase_minus1.md"

    if phase_group == "copy":
        knowhow_files = [
            (f"`{KNOWHOW_MARKETING_PATH}`", "마케팅 노하우", "마케팅 성공+실패 패턴 + 시작 전 체크리스트"),
            (f"`{KNOWHOW_PATH}`", "QC 피드백 로그", "QC 실패/성공 이력"),
        ]
    elif phase_group == "design":
        knowhow_files = [
            (f"`{KNOWHOW_DESIGN_PATH}`", "디자인 노하우", "디자인 성공+실패 패턴 + 시작 전 체크리스트"),
            (f"`{KNOWHOW_PATH}`", "QC 피드백 로그", "QC 실패/성공 이력"),
        ]
    else:  # "final"
        knowhow_files = [
            (f"`{KNOWHOW_PATH}`", "QC 피드백 로그", "QC 실패/성공 이력"),
            (f"`{KNOWHOW_MARKETING_PATH}`", "마케팅 노하우", "마케팅 성공+실패 패턴 + 시작 전 체크리스트"),
            (f"`{KNOWHOW_DESIGN_PATH}`", "디자인 노하우", "디자인 성공+실패 패턴 + 시작 전 체크리스트"),
        ]

    file_count = len(knowhow_files)
    file_list_str = ""
    for i, (path, name, desc) in enumerate(knowhow_files, 1):
        file_list_str += f"{i}. **{name}**: {path}\n" f"   - {desc}\n"

    return (
        f"## Phase -1: 노하우 프리로딩\n"
        f"- 작업 ID: {task_id}\n"
        f"- 담당: 복합팀 팀장 (Opus)\n\n"
        f"## ⚠️ 필독 파일 ({file_count}개 모두 반드시 읽으세요)\n"
        f"{file_list_str}\n"
        f"## 각 노하우 파일 구조 (3섹션)\n"
        f'- 실패 패턴: "이건 하면 안 된다"\n'
        f'- 성공 패턴: "이렇게 했더니 점수가 올랐다"\n'
        f"- 시작 전 체크리스트: 매번 반드시 확인\n\n"
        f"## 작업 절차\n"
        f"1. 위 {file_count}개 파일을 모두 읽으세요.\n"
        f"2. 각 파일의 핵심 요약을 보고서에 기재하세요.\n"
        f"3. 특히 반복 FAIL 패턴과 성공 패턴을 중점 정리하세요.\n\n"
        f"## 완료 기준 (잠금 해제 조건)\n"
        f"- {file_count}개 파일 모두 읽기 완료\n"
        f"- 핵심 요약이 보고서에 기재됨\n"
        f"- KNOWHOW_PRELOAD_OK 토큰 발급 → Phase 0 진행 가능\n\n"
        f"## 노하우 freshness 확인\n"
        f"노하우 파일을 읽은 후, 파일 마지막 수정일을 확인하세요.\n"
        f'- 7일 이상 경과: ⚠️ 경고 — "노하우가 오래됐습니다. 최근 QC 결과가 미반영일 수 있습니다."\n'
        f'- 보고서에 "노하우 최종 수정일: YYYY-MM-DD" 명시\n\n'
        f"## ⚠️ 중요\n"
        f"- 1차 위임뿐 아니라 **2차, 3차 위임 시작 시에도 반복 수행** (세션 간 기억 불가)\n"
        f"- 노하우 핵심 요약을 보고서에 기재해야 Phase 0 토큰 발급\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_phase0_prompt(task_id: str, campaign_type: str = "conversion") -> str:
    """Phase 0: 브리프 검증 게이트

    Args:
        task_id: 작업 ID
        campaign_type: 캠페인 유형 ("conversion" | "brand")

    Returns:
        Phase 0 프롬프트 문자열
    """
    brief_path = f"{WORKSPACE_ROOT}/memory/tasks/{task_id}-brief.md"
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase0.md"

    return (
        f"당신은 마아트(Ma'at), 이미지 제작 QC 게이트 담당자입니다.\n\n"
        f"## Phase 0: 브리프 검증 게이트\n"
        f"- 작업 ID: {task_id}\n"
        f"- 캠페인 유형: {campaign_type}\n"
        f"- 브리프 파일: {brief_path}\n\n"
        f"## 필수 참조 문서\n"
        f"- QC 노하우 문서: {KNOWHOW_PATH}\n"
        f"- 브랜드 가이드라인: {WORKSPACE_ROOT}/memory/specs/brand-guidelines.md\n\n"
        f"## 브리프 필수 필드 체크 (5종 전항목 존재해야 PASS)\n"
        f"브리프 파일을 읽고 아래 5개 필드가 모두 작성되어 있는지 검증하세요:\n\n"
        f"1. **캠페인 목적** — 인지/클릭/전환 중 우선순위 명시 여부\n"
        f"2. **타겟 고객** — 구체적 인물상 기술 여부 (나이/직업/고민 등)\n"
        f"3. **핵심 메시지** — 단일 핵심 메시지(One Message) 명시 여부\n"
        f"4. **캠페인 유형** — conversion 또는 brand 명시 여부\n"
        f"5. **QC 가중치 프로파일** — B 카테고리 가중치 프로파일(A: 전환 최적화 / B: 브랜드 인지도) 선택 여부\n\n"
        f"## 검증 절차\n"
        f"1. {brief_path} 파일을 읽어 위 5개 필드 존재 여부를 확인하세요.\n"
        f"2. 각 필드별 PASS/FAIL 판정 및 미흡 사유를 기록하세요.\n"
        f"3. 5개 필드 전항목 PASS → Phase 1 진행 허가\n"
        f"4. 1개라도 FAIL → 마케팅팀에 브리프 보완 요청 (Phase 1 진행 불가)\n\n"
        f"## 검증 결과 출력 형식\n"
        f"```\n"
        f"[Phase 0 브리프 검증 결과]\n"
        f"- 캠페인 목적: PASS/FAIL — (사유)\n"
        f"- 타겟 고객: PASS/FAIL — (사유)\n"
        f"- 핵심 메시지: PASS/FAIL — (사유)\n"
        f"- 캠페인 유형: PASS/FAIL — (사유)\n"
        f"- QC 가중치 프로파일: PASS/FAIL — (사유)\n"
        f"\n최종 판정: PASS/FAIL\n"
        f"다음 Phase: Phase 1 진행 가능 / 브리프 보완 요청\n"
        f"```\n\n"
        f"에스컬레이션 규칙: memory/specs/escalation-rules.md 참조\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_phase1_prompt(task_id: str, brief_path: str) -> str:
    """Phase 1: 카피/기획 (마케팅팀)

    Args:
        task_id: 작업 ID
        brief_path: 승인된 브리프 파일 경로

    Returns:
        Phase 1 프롬프트 문자열
    """
    output_path = f"{WORKSPACE_ROOT}/memory/tasks/{task_id}-copy-plan.md"
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase1.md"

    return (
        f"당신은 마케팅 팀장입니다.\n\n"
        f"## Phase 1: 카피/기획\n"
        f"- 작업 ID: {task_id}\n"
        f"- 승인된 브리프: {brief_path}\n\n"
        f"## 필수 참조 문서\n"
        f"- 브랜드 가이드라인: {WORKSPACE_ROOT}/memory/specs/brand-guidelines.md\n"
        f"- 마케팅 컨텍스트: {WORKSPACE_ROOT}/memory/specs/marketing-context.md\n"
        f"- QC 노하우 문서: {KNOWHOW_PATH}\n\n"
        f"## 작업 지시\n"
        f"1. {brief_path}를 읽고 브리프를 완전히 이해하세요.\n"
        f"2. 브리프의 캠페인 목적 / 타겟 고객 / 핵심 메시지를 기반으로 카피를 작성하세요.\n"
        f"3. 캠페인 유형(conversion/brand)에 맞는 톤과 CTA를 설계하세요.\n\n"
        f"## 카피 산출물 필수 구성\n"
        f"다음 항목을 포함한 카피 기획서를 `{output_path}`에 저장하세요:\n\n"
        f"1. **헤드라인** (최대 20자) — 주목을 끄는 한 줄 메시지\n"
        f"2. **서브 카피** (최대 40자) — 헤드라인 보충 설명\n"
        f"3. **바디 카피** (최대 80자) — 핵심 내용 전달\n"
        f"4. **CTA 문구** (최대 15자) — 행동 유도 버튼 텍스트\n"
        f"5. **감정 키워드** (3~5개) — 이미지 전반에 흐를 감정 톤\n"
        f"6. **크리에이티브 디렉션 (필수)** — 디자인팀이 이 지시를 보고 이미지를 제작함\n"
        f'   a. 배경 컨셉: 어떤 분위기/장면의 배경을 사용할지 (예: "고급 사무실", "도시 야경", "깨끗한 흰 배경")\n'
        f'   b. 비주얼 톤: 전체 색감/분위기 (예: "골드+네이비 고급스러운", "레드+블랙 긴급한", "따뜻한 브라운")\n'
        f"   c. 사진 방향: 포토리얼 배경 사용 여부, AI 생성 vs 스톡 이미지 vs CSS-only\n"
        f"   d. 레이아웃 배치: 텍스트 위치, CTA 버튼 위치, 로고 위치 제안\n"
        f"   e. 참고 레퍼런스: 비슷한 톤의 기존 배너나 광고 예시 (있는 경우)\n\n"
        f"## 팀원 활용 (Task tool)\n"
        f"- 페이토 (Peitho): 카피라이팅 (model: haiku) — 헤드라인/CTA/카피 초안\n"
        f"- 아폴론 (Apollo): 콘텐츠 크리에이터 (model: haiku) — 감정 키워드/메시지 확장\n\n"
        f"## 완료 기준\n"
        f"- 4개 필수 카피 항목(헤드라인/서브/바디/CTA) 모두 작성 완료\n"
        f"- 크리에이티브 디렉션(배경 컨셉/비주얼 톤/사진 방향/레이아웃 배치) 작성 완료\n"
        f"- 브리프의 핵심 메시지와 일치 여부 자체 검토 완료\n"
        f"- 산출물 파일 저장: {output_path}\n\n"
        f"에스컬레이션 규칙: memory/specs/escalation-rules.md 참조\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
        f"- 완료 후 Phase 2 (카피 QC 게이트)로 핸드오프하세요.\n"
    )


def build_phase2_prompt(
    task_id: str,
    copy_plan_path: str,
    campaign_type: str = "conversion",
) -> str:
    """Phase 2: 카피 QC 게이트 (마아트)

    Args:
        task_id: 작업 ID
        copy_plan_path: 카피 기획서 파일 경로
        campaign_type: 캠페인 유형 ("conversion" | "brand")

    Returns:
        Phase 2 프롬프트 문자열
    """
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase2.md"
    brief_path = f"{WORKSPACE_ROOT}/memory/tasks/{task_id}-brief.md"

    weight_key = "weight_conversion" if campaign_type == "conversion" else "weight_brand"
    b_weighted_items = []
    for code, item in QC_CATEGORY_B.items():
        w = float(str(item.get(weight_key, item["weight_conversion"])))
        b_weighted_items.append(f"  - [{code}] {item['name']}: {int(w * 100)}%")
    b_weighted_str = "\n".join(b_weighted_items)

    return (
        f"당신은 마아트(Ma'at), 이미지 제작 QC 게이트 담당자입니다.\n\n"
        f"## Phase 2: 카피 QC 게이트\n"
        f"- 작업 ID: {task_id}\n"
        f"- 캠페인 유형: {campaign_type}\n"
        f"- 카피 기획서: {copy_plan_path}\n"
        f"- 원본 브리프: {brief_path}\n\n"
        f"## 필수 참조 문서\n"
        f"- QC 노하우 문서: {KNOWHOW_PATH} (필독)\n"
        f"- 브랜드 가이드라인: {WORKSPACE_ROOT}/memory/specs/brand-guidelines.md\n\n"
        f"## QC 기준\n\n"
        f"{_build_category_a_section()}\n"
        f"{_build_category_b_section(campaign_type)}\n"
        f"**카피 단계 적용 B항목 가중치 ({campaign_type} 프로파일):**\n"
        f"{b_weighted_str}\n\n"
        f"{_build_fail_categories_section()}\n"
        f"## 검증 절차\n"
        f"1. {copy_plan_path}를 읽어 카피 내용을 파악하세요.\n"
        f"2. {brief_path}를 읽어 원본 브리프와 카피의 정합성을 확인하세요.\n"
        f"3. 카테고리 A 전항목 자동 검증 수행 (텍스트 길이/규격 준수 여부 등)\n"
        f"4. 카테고리 B 각 항목을 0~100점으로 평가 후 가중 합산\n"
        f"5. 최종 판정:\n"
        f"   - A 전항목 PASS + B 가중 점수 ≥ 70 → Phase 3 진행 허가\n"
        f"   - A 1개라도 FAIL → 즉시 반려, 카피 재작업 요청\n"
        f"   - B 가중 점수 < 70 → 반려, 미흡 항목 명시하여 카피 재작업 요청\n\n"
        f"## 검증 결과 출력 형식\n"
        f"```\n"
        f"[Phase 2 카피 QC 결과]\n"
        f"캠페인 유형: {campaign_type}\n\n"
        f"[카테고리 A]\n"
        f"- A-01 글자 겹침: PASS/FAIL\n"
        f"- A-02 경계 이탈: PASS/FAIL\n"
        f"- A-03 해상도 규격: PASS/FAIL\n"
        f"- A-04 폰트 규칙: PASS/FAIL\n"
        f"- A-05 satori 사용 여부: PASS/FAIL\n"
        f"카테고리 A 판정: PASS/FAIL\n\n"
        f"[카테고리 B — {campaign_type} 가중치]\n"
        f"- B-01 CTA 명확성: (점수)/100 × 가중치 = (가중점수)\n"
        f"- B-02 메시지 일관성: (점수)/100 × 가중치 = (가중점수)\n"
        f"- B-03 브랜드 정합성: (점수)/100 × 가중치 = (가중점수)\n"
        f"- B-04 시각적 계층 구조: (점수)/100 × 가중치 = (가중점수)\n"
        f"- B-05 감정적 공감도: (점수)/100 × 가중치 = (가중점수)\n"
        f"카테고리 B 합산: (합산점수)/100\n"
        f"카테고리 B 판정: PASS(≥70) / FAIL(<70)\n\n"
        f"최종 판정: PASS/FAIL\n"
        f"FAIL 사유: (FAIL_CATEGORIES 코드 명시)\n"
        f"다음 Phase: Phase 3 진행 가능 / 카피 재작업 요청\n"
        f"```\n\n"
        f"에스컬레이션 규칙: memory/specs/escalation-rules.md 참조\n\n"
        f"## 학습 기록 (PASS/FAIL 무관 필수 — 미수행 시 .done 발급 불가)\n\n"
        f"### 기록 대상 파일\n"
        f"1. `{KNOWHOW_MARKETING_PATH}` — 마케팅 성공/실패 패턴\n"
        f"2. `{KNOWHOW_PATH}` — QC 성공/실패 패턴\n\n"
        f"### 필수 기록 항목\n"
        f"각 배너별로:\n"
        f'- 실패 패턴: "이건 해서 점수 떨어졌다" (구체적 행동 + B 항목 + 감점 점수)\n'
        f'- 성공 패턴: "이렇게 개선했더니 올랐다" (구체적 행동 + B 항목 + 점수 변화)\n'
        f"- 체크리스트 추가: 반복 FAIL 항목을 체크리스트에 추가\n\n"
        f"### 검증 규칙\n"
        f"보고서에 아래 섹션 필수 포함:\n"
        f"```\n"
        f"## 노하우 업데이트 기록\n"
        f"- knowhow-marketing.md: 추가 패턴 N건 (성공 X건, 실패 Y건)\n"
        f"- design-qc-knowhow.md: 추가 패턴 N건\n"
        f"- 체크리스트 추가: N항목\n"
        f"```\n"
        f'이 섹션이 없거나 "0건"이면 보고서 불합격 처리.\n\n'
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_phase3_prompt(task_id: str, approved_plan_path: str) -> str:
    """Phase 3: 디자인 실행 (디자인팀)

    Args:
        task_id: 작업 ID
        approved_plan_path: QC 승인된 카피 기획서 파일 경로

    Returns:
        Phase 3 프롬프트 문자열
    """
    output_dir = f"{WORKSPACE_ROOT}/memory/outputs/{task_id}"
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase3.md"

    return (
        f"당신은 아마테라스(Amaterasu), 디자인 팀장입니다.\n\n"
        f"## Phase 3: 디자인 실행\n"
        f"- 작업 ID: {task_id}\n"
        f"- QC 승인된 카피 기획서: {approved_plan_path}\n"
        f"- 이미지 출력 디렉토리: {output_dir}/\n\n"
        f"## 필수 참조 문서 (작업 전 반드시 읽으세요)\n"
        f"- QC 노하우 문서: {KNOWHOW_PATH} ← **필독. 과거 FAIL 패턴 숙지 후 작업 시작**\n"
        f"- 브랜드 가이드라인: {WORKSPACE_ROOT}/memory/specs/brand-guidelines.md\n"
        f"- 디자인 팀 상세: {WORKSPACE_ROOT}/memory/org-details/design-team.json\n\n"
        f"## ⛔ satori 금지 규칙 (절대 위반 금지)\n"
        f"- **satori-cardnews 스킬 사용 엄금**\n"
        f"- satori 기반 렌더링은 이 워크플로우에서 허용되지 않습니다.\n"
        f"- 사용 시 자동으로 QC 카테고리 A-05 FAIL 처리되며 재작업 필요.\n"
        f"- 대체 방법: hybrid-image 스킬(이나리) 또는 canvas-design 스킬(카구야) 사용\n\n"
        f"## 폰트 규칙 (A-04, 반드시 준수)\n"
        f"- 헤드라인: **{_HEADLINE_MIN}px**\n"
        f"- 서브 카피: **{_SUBHEAD_MIN}px**\n"
        f"- 바디 카피: **{ABSOLUTE_MIN_PX}px**\n"
        f"- 위 규격 외 크기 사용 시 A-04 FAIL\n\n"
        f"## 폰트 두께 규칙 (A-09, 반드시 준수)\n"
        f"- **font-weight {_MAX_BANNED_WEIGHT} 이하(light/thin) 사용 금지**\n"
        f"- 최소 {_MIN_ALLOWED_WEIGHT}(regular) 이상만 허용\n"
        f"- 위반 시 A-09 FAIL\n\n"
        f"## 대비율 규칙 (A-07, WCAG AAA 기준)\n"
        f"- 일반 텍스트: **7:1** 이상\n"
        f"- 대형 텍스트(18pt+/14pt bold+): **4.5:1** 이상\n"
        f"- 기존 AA 기준에서 AAA로 상향 — 가독성 최우선\n\n"
        f"## 반투명 레이어 + 글래스모피즘 가이드라인\n"
        f"- **일반 오버레이**: 배경과 텍스트 사이에 반투명 레이어 적용 (opacity 0.4~0.6)\n"
        f"- **글래스모피즘 카드**: opacity 0.15~0.30 + backdrop-filter blur로 뒷 이미지 비침 허용\n"
        f"  - 단, 가독성 최우선: 카드 배경색 조정으로 텍스트 대비율 확보 (완전 투명 금지)\n"
        f"  - backdrop-filter: blur(10px~20px) 권장\n"
        f"- 텍스트 영역에는 반드시 반투명 오버레이 확보 (글자 겹침 방지)\n\n"
        f"## 서울대보험쌤 로고 사용 규칙\n"
        f"- 텍스트 '서울대보험쌤' 대신 로고 이미지 사용\n"
        f"- 로고 원본: {WORKSPACE_ROOT}/assets/brand/logo-snuinsurance.jpg\n"
        f"- 어두운 배경: 흰색 버전 사용 → {WORKSPACE_ROOT}/assets/brand/logo-snuinsurance-white.png\n"
        f"- 밝은 배경: 원본 로고 그대로 사용\n\n"
        f"## 안전 영역 규칙 (A-01, A-02)\n"
        f"- 모든 텍스트/요소는 이미지 가장자리에서 최소 40px 이상 여백 확보\n"
        f"- 텍스트 요소 간 최소 8px 이상 간격 유지\n\n"
        f"## 팀원 라우팅 (Task tool)\n"
        f"- 이나리 (Inari): hybrid-image 스킬 — AI 배경 + HTML 텍스트 오버레이 (model: sonnet)\n"
        f"- 카구야 (Kaguya): canvas-design 스킬 — PDF/PNG 아트워크 (model: sonnet)\n"
        f"- 비너스 (Venus): gemini-image 스킬 — 포토리얼 배경 생성 (횡단조직 소환)\n"
        f"⚠️ 벤자이텐(satori-cardnews)은 이 워크플로우에서 호출 금지\n\n"
        f"## 작업 절차\n"
        f"1. {approved_plan_path}를 읽어 카피 기획서 내용 파악\n"
        f"2. {KNOWHOW_PATH}를 읽어 과거 FAIL 패턴 숙지\n"
        f"3. 적절한 팀원에게 Task tool로 디자인 위임\n"
        f"4. 산출 이미지를 {output_dir}/에 저장\n"
        f"5. ⛔ **PNG 렌더링 필수**: HTML 배너 생성 시 반드시 Playwright로 PNG 렌더링까지 완료\n"
        f"   - render_banners.py 등 렌더링 스크립트 생성 시 즉시 실행\n"
        f"   - .html만 있고 .png가 없으면 미완료 처리됨\n"
        f"6. 저장된 이미지 파일 경로 목록을 보고서에 명시 (.png 파일 경로)\n\n"
        f"## 완료 기준\n"
        f"- ⛔ **PNG 파일 필수**: 출력 디렉토리에 .png 파일이 반드시 존재해야 함\n"
        f"- ⛔ **HTML만 있고 PNG 없음 = 미완료**: .html 파일만 있고 대응 .png가 없으면 Phase 3 미완료 처리\n"
        f"- Playwright로 HTML → PNG 렌더링 완료 후 산출물 경로에 .png 파일 확인\n"
        f"- satori 미사용 확인\n"
        f"- 폰트 규칙 자체 점검 완료 (크기 + 두께)\n"
        f"- 안전 영역 자체 점검 완료\n"
        f"- 대비율 WCAG AAA 기준 충족 확인\n"
        f"- 반투명 레이어/글래스모피즘 가독성 확인\n"
        f"- 서울대보험쌤 로고 이미지 사용 확인 (텍스트 대체)\n\n"
        f"에스컬레이션 규칙: memory/specs/escalation-rules.md 참조\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
        f"- 산출물 파일 경로 목록을 개별로 명시하세요 (축약형 금지, 예: slide{{1-5}}.png 불가)\n"
        f"- 완료 후 Phase 4 (디자인 QC 게이트)로 핸드오프하세요.\n"
    )


def build_phase4_prompt(
    task_id: str,
    image_paths: list[str],
    campaign_type: str = "conversion",
) -> str:
    """Phase 4: 디자인 QC 게이트 (마아트)

    Args:
        task_id: 작업 ID
        image_paths: 검수할 이미지 파일 경로 목록
        campaign_type: 캠페인 유형 ("conversion" | "brand")

    Returns:
        Phase 4 프롬프트 문자열
    """
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase4.md"
    copy_plan_path = f"{WORKSPACE_ROOT}/memory/tasks/{task_id}-copy-plan.md"

    image_list_str = "\n".join(f"  - {p}" for p in image_paths)
    image_count = len(image_paths)

    weight_key = "weight_conversion" if campaign_type == "conversion" else "weight_brand"
    b_weighted_items = []
    for code, item in QC_CATEGORY_B.items():
        w = float(str(item.get(weight_key, item["weight_conversion"])))
        b_weighted_items.append(f"  - [{code}] {item['name']}: {int(w * 100)}%")
    b_weighted_str = "\n".join(b_weighted_items)

    return (
        f"당신은 마아트(Ma'at), 이미지 제작 QC 게이트 담당자입니다.\n\n"
        f"## Phase 4: 디자인 QC 게이트\n"
        f"- 작업 ID: {task_id}\n"
        f"- 캠페인 유형: {campaign_type}\n"
        f"- 검수 이미지 수: {image_count}개\n\n"
        f"## 검수 대상 이미지 (실제 이미지를 직접 열어서 육안 평가하세요)\n"
        f"{image_list_str}\n\n"
        f"## ⚠️ 중요: 실제 이미지 시각 평가 지시\n"
        f"- 위 이미지 파일을 반드시 **직접 열어서(Read tool 또는 이미지 뷰어)** 시각적으로 평가하세요.\n"
        f"- 파일 경로만 보고 판단하지 마세요. 실제 렌더링 결과를 확인하세요.\n"
        f"- 각 이미지에 대해 개별 QC 판정을 수행하세요.\n\n"
        f"## 필수 참조 문서\n"
        f"- QC 노하우 문서: {KNOWHOW_PATH} (필독)\n"
        f"- 원본 카피 기획서: {copy_plan_path}\n"
        f"- 브랜드 가이드라인: {WORKSPACE_ROOT}/memory/specs/brand-guidelines.md\n\n"
        f"## QC 기준\n\n"
        f"{_build_category_a_section()}\n"
        f"{_build_category_b_section(campaign_type)}\n"
        f"**디자인 단계 적용 B항목 가중치 ({campaign_type} 프로파일):**\n"
        f"{b_weighted_str}\n\n"
        f"{_build_fail_categories_section()}\n"
        f"## 검증 절차\n"
        f"1. 각 이미지 파일을 직접 열어 시각 평가\n"
        f"2. 이미지별로 카테고리 A 전항목 검증 (글자 겹침/경계 이탈/해상도/폰트/satori 여부)\n"
        f"3. 카피 기획서({copy_plan_path})와 대조하여 카테고리 B 평가\n"
        f"4. 이미지별 최종 판정 및 FAIL 사유 코드 명시\n"
        f"5. 전체 판정:\n"
        f"   - 전 이미지 PASS → 워크플로우 완료 승인\n"
        f"   - 1개 이상 FAIL → 해당 이미지 재작업 요청 (Phase 3 루프)\n\n"
        f"## 검증 결과 출력 형식 (이미지별 반복)\n"
        f"```\n"
        f"[Phase 4 디자인 QC 결과]\n"
        f"캠페인 유형: {campaign_type}\n\n"
        f"=== 이미지 1: (파일 경로) ===\n"
        f"[카테고리 A]\n"
        f"- A-01 글자 겹침: PASS/FAIL — (근거)\n"
        f"- A-02 경계 이탈: PASS/FAIL — (근거)\n"
        f"- A-03 해상도 규격: PASS/FAIL — (근거)\n"
        f"- A-04 폰트 규칙: PASS/FAIL — (근거)\n"
        f"- A-05 satori 사용 여부: PASS/FAIL — (근거)\n"
        f"카테고리 A: PASS/FAIL\n\n"
        f"[카테고리 B — {campaign_type} 가중치]\n"
        f"- B-01 CTA 명확성: (점수)/100 × 가중치 = (가중점수) — (근거)\n"
        f"- B-02 메시지 일관성: (점수)/100 × 가중치 = (가중점수) — (근거)\n"
        f"- B-03 브랜드 정합성: (점수)/100 × 가중치 = (가중점수) — (근거)\n"
        f"- B-04 시각적 계층 구조: (점수)/100 × 가중치 = (가중점수) — (근거)\n"
        f"- B-05 감정적 공감도: (점수)/100 × 가중치 = (가중점수) — (근거)\n"
        f"카테고리 B 합산: (합산점수)/100 → PASS(≥70)/FAIL(<70)\n\n"
        f"이미지 판정: PASS/FAIL\n"
        f"FAIL 사유: (FAIL_CATEGORIES 코드)\n\n"
        f"=== 전체 최종 판정 ===\n"
        f"PASS 이미지: (n)/{image_count}\n"
        f"FAIL 이미지: (n)/{image_count}\n"
        f"워크플로우 결과: 완료 승인 / Phase 3 재작업 요청\n"
        f"재작업 대상: (FAIL 이미지 경로 목록)\n"
        f"```\n\n"
        f"에스컬레이션 규칙: memory/specs/escalation-rules.md 참조\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_phase0_5_prompt(task_id: str) -> str:
    """Phase 0.5: 브리프 QC (마아트)

    5항목 100점 만점, 85점 이상 통과.
    """
    brief_path = f"{WORKSPACE_ROOT}/memory/tasks/{task_id}-brief.md"
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase0_5.md"

    return (
        f"당신은 마아트(Ma'at), 이미지 제작 QC 게이트 담당자입니다.\n\n"
        f"## Phase 0.5: 브리프 QC\n"
        f"- 작업 ID: {task_id}\n"
        f"- 브리프 파일: {brief_path}\n\n"
        f"## 필수 참조 문서\n"
        f"- QC 노하우 문서: {KNOWHOW_PATH}\n\n"
        f"## 브리프 QC 체크리스트 (100점 만점, 85점 이상 통과)\n\n"
        f"| # | 항목 | 배점 | 판정 기준 |\n"
        f"|---|------|------|----------|\n"
        f"| BQ-01 | 타겟 1인칭 명시 | 20점 | 구체적 인구통계/심리통계 명시 여부 |\n"
        f"| BQ-02 | 핵심 메시지 단수 여부 | 20점 | 핵심 이익 문장 1개만 존재 (복수이면 0점) |\n"
        f"| BQ-03 | 생산 가능성 — 텍스트 길이 | 20점 | 헤드라인 ≤ 20자, 서브카피 ≤ 40자 명시 |\n"
        f"| BQ-04 | 캠페인 목표 KPI 명시 | 20점 | CTR/상담신청/브랜드인지 중 1개 명시 |\n"
        f"| BQ-05 | 금지 패턴 없음 | 20점 | 이전 FAIL 패턴 미포함 확인 (knowhow 파일의 실패 패턴 참조; 예: 과장 표현, 법적 위험, 경쟁사 직접 비교) |\n\n"
        f"## 판정 로직\n"
        f"- 85점 이상: 브리프 승인 → Phase 1 진행, BRIEF_QC_OK 토큰 발급\n"
        f"- 84점 이하: 브리프 반려 → Phase 0 재작성\n\n"
        f"## 검증 결과 출력 형식\n"
        f"```\n"
        f"[Phase 0.5 브리프 QC 결과]\n"
        f"- BQ-01 타겟 1인칭: X/20점 — (사유)\n"
        f"- BQ-02 핵심 메시지 단수: X/20점 — (사유)\n"
        f"- BQ-03 텍스트 길이: X/20점 — (사유)\n"
        f"- BQ-04 KPI 명시: X/20점 — (사유)\n"
        f"- BQ-05 금지 패턴: X/20점 — (사유)\n"
        f"\n총점: X/100\n"
        f"판정: 승인/반려\n"
        f"토큰: BRIEF_QC_OK 발급/미발급\n"
        f"```\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_phase1_5_prompt(
    task_id: str,
    copy_draft_path: str,
    cycle: int = 1,
) -> str:
    """Phase 1.5: 기획 QC (마아트 + 로키 공동 (3자 평가))

    7항목 100점 만점.
    PASS(85점)까지 반복. 모든 Cycle 동일 기준.
    """
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase1_5.md"
    brief_path = f"{WORKSPACE_ROOT}/memory/tasks/{task_id}-brief.md"

    threshold = 85
    immediate_pass = ESCALATION_RULES["plan_qc_immediate_pass"]

    cycle_focus = {
        1: "방향성 확인 (PQ-01,PQ-02 필수 만점)",
        2: "톤 정제 (PQ-04,PQ-06 필수 만점, 이전 사이클 대비 +14점 이상)",
        3: "최적화 (전 항목 기준 상향)",
    }
    focus = cycle_focus.get(cycle, "품질 정제")

    return (
        f"당신은 마아트(Ma'at), 이미지 제작 QC 게이트 담당자입니다.\n"
        f"로키(역분석)가 공동 검토합니다. (3자 평가: 만든 팀 ≠ 평가 팀)\n\n"
        f"## Phase 1.5: 기획 QC\n"
        f"- 작업 ID: {task_id}\n"
        f"- 사이클: {cycle} (PASS까지 반복)\n"
        f"- 통과 임계값: {threshold}/100점\n"
        f"- 중점: {focus}\n"
        f"- 카피 초안: {copy_draft_path}\n"
        f"- 원본 브리프: {brief_path}\n\n"
        f"## 필수 참조 문서\n"
        f"- QC 노하우 문서: {KNOWHOW_PATH}\n\n"
        f"## 기획 QC 체크리스트 (100점 만점)\n\n"
        f"| # | 항목 | 배점 | 판정 기준 |\n"
        f"|---|------|------|----------|\n"
        f"| PQ-01 | 메시지 계층 구조 | 15점 | 헤드라인→서브카피→CTA 논리적 흐름 |\n"
        f"| PQ-02 | 감정적 훅 존재 | 15점 | 첫 문장에서 타겟의 불안/욕망/호기심 건드림 |\n"
        f"| PQ-03 | CTA 명시 | 14점 | 구체적 행동 유도 문구 존재 |\n"
        f"| PQ-04 | 디자인 실현 가능성 | 14점 | 카피가 Zone 명세서에 맞게 들어갈 수 있음 (로키 검토) |\n"
        f"| PQ-05 | 금지 패턴 해당 없음 | 14점 | 수동적 문장, 모호한 혜택, 금지어 없음 |\n"
        f"| PQ-06 | 브랜드 톤앤매너 준수 | 14점 | 신뢰감/친근함/전문성 중 적합한 톤 선택 |\n"
        f"| PQ-07 | 세계 최고 전문가/프로 수준 | 14점 | 어떤 누가 평가해도 세계 최고의 전문가/프로 수준인가? |\n\n"
        f"## 판정 로직\n"
        f"- {threshold}점 이상: 기획 승인 → Phase 2 진행, PLAN_QC_OK 토큰 발급\n"
        f"- {threshold-1}점 이하: 기획 반려 → Phase 1 재기획\n"
        f"- **{immediate_pass}점 이상 = 즉시 PASS** (어떤 사이클이든, 노하우 축적 효과)\n\n"
        f"## 학습 기록 (PASS/FAIL 무관 필수 — 미수행 시 .done 발급 불가)\n\n"
        f"### 기록 대상 파일\n"
        f"1. `{KNOWHOW_MARKETING_PATH}` — 마케팅 성공/실패 패턴\n"
        f"2. `{KNOWHOW_PATH}` — QC 성공/실패 패턴\n\n"
        f"### 필수 기록 항목\n"
        f"각 배너별로:\n"
        f'- 실패 패턴: "이건 해서 점수 떨어졌다" (구체적 행동 + PQ 항목 + 감점 점수)\n'
        f'- 성공 패턴: "이렇게 개선했더니 올랐다" (구체적 행동 + PQ 항목 + 점수 변화)\n'
        f"- 체크리스트 추가: 반복 FAIL 항목을 체크리스트에 추가\n\n"
        f"### 검증 규칙\n"
        f"보고서에 아래 섹션 필수 포함:\n"
        f"```\n"
        f"## 노하우 업데이트 기록\n"
        f"- knowhow-marketing.md: 추가 패턴 N건 (성공 X건, 실패 Y건)\n"
        f"- design-qc-knowhow.md: 추가 패턴 N건\n"
        f"- 체크리스트 추가: N항목\n"
        f"```\n"
        f'이 섹션이 없거나 "0건"이면 보고서 불합격 처리.\n\n'
        f"## 검증 결과 출력 형식\n"
        f"```\n"
        f"[Phase 1.5 기획 QC 결과 — Cycle {cycle}]\n"
        f"- PQ-01 메시지 계층: X/15점 — (사유)\n"
        f"- PQ-02 감정적 훅: X/15점 — (사유)\n"
        f"- PQ-03 CTA: X/14점 — (사유)\n"
        f"- PQ-04 디자인 실현성: X/14점 — (로키 의견)\n"
        f"- PQ-05 금지 패턴: X/14점 — (사유)\n"
        f"- PQ-06 톤앤매너: X/14점 — (사유)\n"
        f"- PQ-07 세계 최고 수준: X/14점 — (사유)\n"
        f"\n총점: X/100\n"
        f"이전 사이클 점수: {'N/A' if cycle == 1 else '(이전 점수)'}\n"
        f"판정: 승인/반려\n"
        f"토큰: PLAN_QC_OK 발급/미발급\n"
        f"```\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_phase2_5_prompt(
    task_id: str,
    pilot_image_paths: list[str],
) -> str:
    """Phase 2.5: 파일럿 QC 게이트 (아마테라스 + 로키)

    극단 케이스 3종 검증. 3개 모두 통과 시 배치 생산 승인.
    """
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase2_5.md"
    image_list = "\n".join(f"  - {p}" for p in pilot_image_paths)

    return (
        f"당신은 아마테라스(디자인팀장) + 로키(보안팀장, Devil's Advocate) 공동 검증입니다.\n\n"
        f"## Phase 2.5: 파일럿 QC 게이트\n"
        f"- 작업 ID: {task_id}\n"
        f"- 파일럿 이미지 수: {len(pilot_image_paths)}개\n\n"
        f"## 파일럿 이미지 목록\n{image_list}\n\n"
        f"## 극단 케이스 선정 기준\n"
        f"1. 텍스트 밀도 극단값: 헤드라인 글자수/max_chars 비율 가장 높은 것\n"
        f"2. Zone 겹침 IoU 극단값: 이미지 요소 bbox와 텍스트 Zone bbox의 IoU 가장 높은 것\n"
        f"3. 폰트 크기 최소값 극단값: font_size_range.min이 가장 작은 것\n\n"
        f"## 검증 항목\n"
        f"각 파일럿 이미지에 대해:\n"
        f"- 자동 QC A-01~A-09 전항목 실행\n"
        f"- 텍스트 오버플로우 0건 확인\n"
        f"- 대비율 worst-case pixel WCAG AAA 7:1 (대형텍스트 4.5:1) 이상 확인\n"
        f"- 폰트 크기 최소 {ABSOLUTE_MIN_PX}px 이상 확인 (예외 없음, 페이지 인디케이터·브랜드 태그·면책 문구 포함)\n"
        f"- 아마테라스 시각적 OK 사인\n\n"
        f"## 판정\n"
        f"- 3개 모두 통과 → PILOT_OK 토큰 발급, 배치 생산 승인\n"
        f"- 1개라도 실패 → PILOT_FAIL, 배치 중단 + 원인 수정 후 재파일럿\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_phase3_5_prompt(
    task_id: str,
    image_paths: list[str],
    cycle: int = 1,
) -> str:
    """Phase 3.5: 디자인 QC (로키 단독, Devil's Advocate 평가, model: claude-opus-4-6)

    디자인 퀄리티 QC 10항목 100점 만점.
    모든 Cycle 동일 기준: PASS_THRESHOLD점. PASS까지 반복.
    ESCALATION_RULES["design_qc_immediate_pass"]점 이상 = 즉시 PASS (어떤 사이클이든)
    """
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase3_5.md"
    copy_plan_path = f"{WORKSPACE_ROOT}/memory/tasks/{task_id}-copy-plan.md"

    image_list_str = "\n".join(f"  - {p}" for p in image_paths)
    image_count = len(image_paths)

    threshold = PASS_THRESHOLD
    immediate_pass = ESCALATION_RULES["design_qc_immediate_pass"]

    cycle_focus = {
        1: "기본 품질 확인 (레퍼런스 재현도 + 시각적 계층)",
        2: "품질 정제 (피드 차별화 + 감정적 임팩트 강화)",
        3: f"최종 검증 ({PASS_THRESHOLD}점 이상 필수)",
    }
    focus = cycle_focus.get(cycle, "품질 정제")

    return (
        f"당신은 로키(Loki), 보안팀(레드팀) 팀장입니다. Devil's Advocate로서 디자인 약점을 비판적 시각으로 공격합니다.\n"
        f"(3자 평가: 만든 팀 ≠ 평가 팀, model: claude-opus-4-6)\n\n"
        f"## Phase 3.5: 디자인 QC\n"
        f"- 작업 ID: {task_id}\n"
        f"- 사이클: {cycle} (PASS까지 반복)\n"
        f"- 통과 임계값: {threshold}/100점\n"
        f"- 즉시 PASS: {immediate_pass}점 이상 (어떤 사이클이든)\n"
        f"- 중점: {focus}\n"
        f"- 검수 이미지 수: {image_count}개\n\n"
        f"## 검수 대상 이미지\n"
        f"{image_list_str}\n\n"
        f"## 필수 참조 문서\n"
        f"- QC 노하우 문서: {KNOWHOW_PATH}\n"
        f"- 디자인 노하우: {KNOWHOW_DESIGN_PATH}\n"
        f"- 원본 카피 기획서: {copy_plan_path}\n\n"
        f"## 기술 QC: A 카테고리 9항목 (전항목 PASS 필수)\n"
        f"{_build_category_a_section()}\n"
        f"## 디자인 퀄리티 QC 체크리스트 (100점 만점)\n\n"
        f"| # | 항목 | 배점 | 판정 기준 |\n"
        f"|---|------|------|----------|\n"
        f"| DQ-01 | 레퍼런스 재현도 | 10점 | 디자인 디렉션 대비 재현도 |\n"
        f"| DQ-02 | 시각적 계층 구조 | 10점 | 시선 흐름이 자연스럽고 정보 우선순위가 명확한가 |\n"
        f"| DQ-03 | 프로 완성도 | 10점 | 세계 최고 전문가/프로 수준인가 |\n"
        f"| DQ-04 | 피드 차별화 | 10점 | SNS 피드에서 스크롤을 멈추게 하는 차별화 요소 |\n"
        f"| DQ-05 | 감정적 임팩트 | 10점 | 타겟 고객의 감정을 건드리는 임팩트 |\n"
        f"| DQ-06 | 여백/레이아웃 밸런스 | 10점 | 빈 공간과 콘텐츠 영역의 균형, 시각적 안정감 |\n"
        f"| DQ-07 | 색상 가시성/조화 | 10점 | 색상 선택의 명확성, 가시성, 브랜드 컬러 활용 |\n"
        f"| DQ-08 | 타이포그래피 품질 | 10점 | 폰트 조합, 크기 위계, 자간/행간, 가독성 |\n"
        f"| DQ-09 | CTA 효과성 | 10점 | 버튼 크기/위치/색상이 클릭 유도하는지 |\n"
        f"| DQ-10 | 브랜드 일관성 | 10점 | 같은 대분류 내 세트 간 톤/무드/컬러 통일감 |\n\n"
        f"## 통과 규칙\n"
        f"- **{immediate_pass}점 이상 = 즉시 PASS** (어떤 사이클이든, 노하우 축적 효과)\n"
        f"- 모든 Cycle: {PASS_THRESHOLD}점 이상 → PASS\n"
        f"- Cycle 제한 없음 — PASS까지 반복\n\n"
        f"## FAIL 시 처리\n"
        f"- Phase 3 리턴 + 개선 포인트 노하우에 즉시 기록\n"
        f"- FAIL 사유를 FAIL_CATEGORIES 코드로 명시\n\n"
        f"## 학습 기록 (PASS/FAIL 무관 필수 — 미수행 시 .done 발급 불가)\n\n"
        f"### 기록 대상 파일\n"
        f"1. `{KNOWHOW_DESIGN_PATH}` — 디자인 성공/실패 패턴 + **성공 템플릿 라이브러리**\n"
        f"2. `{KNOWHOW_PATH}` — QC 성공/실패 패턴 + **DQ 항목별 달성 패턴**\n\n"
        f"### 필수 기록 항목\n"
        f"각 배너별로:\n"
        f"- 실패 패턴: DQ 항목 + 감점 점수 + 구체적 CSS 값 (예: `opacity: 0.45` → DQ-03 7점)\n"
        f"- 성공 템플릿: DQ 항목 + 달성 점수 + **복사 가능한 CSS/HTML 코드 블록** + 적용 조건(배경 밝기, 사이즈)\n"
        f'- 성공 템플릿은 `{KNOWHOW_DESIGN_PATH}` "성공 템플릿 라이브러리" 섹션에 추가\n'
        f"- 체크리스트 추가: 반복 FAIL 항목을 체크리스트에 추가\n\n"
        f"### 검증 규칙\n"
        f"보고서에 아래 섹션 필수 포함:\n"
        f"```\n"
        f"## 노하우 업데이트 기록\n"
        f"- knowhow-design.md: 추가 패턴 N건 (성공 X건, 실패 Y건)\n"
        f"- design-qc-knowhow.md: 추가 패턴 N건\n"
        f"- 체크리스트 추가: N항목\n"
        f"```\n"
        f'이 섹션이 없거나 "0건"이면 보고서 불합격 처리.\n\n'
        f"## 검증 결과 출력 형식\n"
        f"```\n"
        f"[Phase 3.5 디자인 QC 결과 — Cycle {cycle}]\n"
        f"[A 카테고리 기술 QC]: 전항목 PASS/FAIL\n\n"
        f"[디자인 퀄리티 QC]\n"
        f"- DQ-01 레퍼런스 재현도: X/10점 — (로키 판정 사유)\n"
        f"- DQ-02 시각적 계층 구조: X/10점 — (사유)\n"
        f"- DQ-03 프로 완성도: X/10점 — (사유)\n"
        f"- DQ-04 피드 차별화: X/10점 — (사유)\n"
        f"- DQ-05 감정적 임팩트: X/10점 — (사유)\n"
        f"- DQ-06 여백/레이아웃 밸런스: X/10점 — (사유)\n"
        f"- DQ-07 색상 가시성/조화: X/10점 — (사유)\n"
        f"- DQ-08 타이포그래피 품질: X/10점 — (사유)\n"
        f"- DQ-09 CTA 효과성: X/10점 — (사유)\n"
        f"- DQ-10 브랜드 일관성: X/10점 — (사유)\n"
        f"\n총점: X/100\n"
        f"이전 사이클 점수: {'N/A' if cycle == 1 else '(이전 점수)'}\n"
        f"판정: PASS/FAIL\n"
        f"토큰: DESIGN_QC_OK 발급/미발급\n"
        f"```\n\n"
        f"에스컬레이션 규칙: memory/specs/escalation-rules.md 참조\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_phase5_prompt(task_id: str) -> str:
    """Phase 5: 노하우 종합 검토

    담당: 복합팀 팀장
    Phase 1.5/3.5에서 실시간 기록한 노하우를 종합 검토 + 누락 보충 + Phase 간 교차 인사이트 정리.
    """
    report_path = f"{WORKSPACE_ROOT}/memory/reports/{task_id}-phase5.md"

    return (
        f"## Phase 5: 노하우 종합 검토\n"
        f"- 작업 ID: {task_id}\n"
        f"- 담당: 복합팀 팀장\n\n"
        f"## 목적\n"
        f"Phase 1.5/3.5에서 실시간 기록한 노하우를 종합 검토하고,\n"
        f"누락된 내용을 보충하며, Phase 간 교차 인사이트를 정리합니다.\n\n"
        f"## 검토 대상 파일 (3개)\n"
        f"1. `{KNOWHOW_PATH}` — QC 피드백 로그\n"
        f"2. `{KNOWHOW_MARKETING_PATH}` — 마케팅 노하우\n"
        f"3. `{KNOWHOW_DESIGN_PATH}` — 디자인 노하우\n\n"
        f"## 작업 절차\n"
        f"1. Phase 1.5 학습 기록 확인: knowhow-marketing.md에 실패+성공+체크리스트 3종이 빠짐없이 기록되었는지\n"
        f"2. Phase 3.5 학습 기록 확인: knowhow-design.md에 실패+성공+체크리스트 3종이 빠짐없이 기록되었는지\n"
        f"3. 누락 보충: 기록이 빠진 사이클이 있으면 보충 기재\n"
        f"4. Phase 간 교차 인사이트 정리:\n"
        f'   - "카피에서 이 방향으로 갔더니 디자인에서도 잘 됐다" 등\n'
        f"   - 카피 ↔ 디자인 간 시너지/충돌 패턴\n"
        f"5. 노하우 3개 파일 정합성 점검:\n"
        f"   - 중복 제거, 모순 해소\n"
        f"   - 다음 작업의 Phase -1에서 즉시 활용 가능한 상태로 정리\n\n"
        f"## 완료 기준\n"
        f"- 3개 파일 모두 검토 완료\n"
        f"- 누락 항목 0건 (보충 완료)\n"
        f"- 교차 인사이트 최소 1건 이상 기재\n"
        f"- 파일 정합성 검증 완료\n\n"
        f"## 보고서\n"
        f"- 보고서 경로: {report_path}\n"
    )


def build_image_workflow_prompt(task_id: str, phase: int | float, **kwargs: object) -> str:
    """Phase별 프롬프트 라우터

    Args:
        task_id: 작업 ID
        phase: Phase 번호 (-1, 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5)
        **kwargs: Phase별 추가 인자
            - phase=-1: (없음)
            - phase=0: campaign_type (str, 기본값: "conversion")
            - phase=0.5: (없음)
            - phase=1: brief_path (str, 필수)
            - phase=1.5: copy_draft_path (str, 필수), cycle (int, 기본값: 1)
            - phase=2: copy_plan_path (str, 필수), campaign_type (str, 기본값: "conversion")
            - phase=2.5: pilot_image_paths (list[str], 필수)
            - phase=3: approved_plan_path (str, 필수)
            - phase=3.5: image_paths (list[str], 필수), cycle (int, 기본값: 1)
            - phase=4: image_paths (list[str], 필수), campaign_type (str, 기본값: "conversion")
            - phase=5: (없음)

    Returns:
        해당 Phase 프롬프트 문자열

    Raises:
        ValueError: 지원하지 않는 Phase 번호 또는 필수 인자 누락 시
    """
    if phase == -1:
        phase_group = str(kwargs.get("phase_group", "final"))
        return build_phase_minus1_prompt(task_id=task_id, phase_group=phase_group)

    elif phase == 0:
        campaign_type = str(kwargs.get("campaign_type", "conversion"))
        return build_phase0_prompt(task_id=task_id, campaign_type=campaign_type)

    elif phase == 0.5:
        return build_phase0_5_prompt(task_id=task_id)

    elif phase == 1:
        brief_path = kwargs.get("brief_path")
        if not brief_path:
            raise ValueError("Phase 1 requires 'brief_path' kwarg")
        return build_phase1_prompt(task_id=task_id, brief_path=str(brief_path))

    elif phase == 1.5:
        copy_draft_path = kwargs.get("copy_draft_path")
        if not copy_draft_path:
            raise ValueError("Phase 1.5 requires 'copy_draft_path' kwarg")
        cycle = int(str(kwargs.get("cycle", 1)))
        return build_phase1_5_prompt(
            task_id=task_id,
            copy_draft_path=str(copy_draft_path),
            cycle=cycle,
        )

    elif phase == 2:
        copy_plan_path = kwargs.get("copy_plan_path")
        if not copy_plan_path:
            raise ValueError("Phase 2 requires 'copy_plan_path' kwarg")
        campaign_type = str(kwargs.get("campaign_type", "conversion"))
        return build_phase2_prompt(
            task_id=task_id,
            copy_plan_path=str(copy_plan_path),
            campaign_type=campaign_type,
        )

    elif phase == 2.5:
        pilot_image_paths = kwargs.get("pilot_image_paths")
        if not pilot_image_paths:
            raise ValueError("Phase 2.5 requires 'pilot_image_paths' kwarg (non-empty list)")
        if not isinstance(pilot_image_paths, list):
            raise ValueError("Phase 2.5 'pilot_image_paths' must be a list of strings")
        return build_phase2_5_prompt(
            task_id=task_id,
            pilot_image_paths=[str(p) for p in pilot_image_paths],
        )

    elif phase == 3:
        approved_plan_path = kwargs.get("approved_plan_path")
        if not approved_plan_path:
            raise ValueError("Phase 3 requires 'approved_plan_path' kwarg")
        return build_phase3_prompt(
            task_id=task_id,
            approved_plan_path=str(approved_plan_path),
        )

    elif phase == 4:
        image_paths = kwargs.get("image_paths")
        if not image_paths:
            raise ValueError("Phase 4 requires 'image_paths' kwarg (non-empty list)")
        if not isinstance(image_paths, list):
            raise ValueError("Phase 4 'image_paths' must be a list of strings")
        campaign_type = str(kwargs.get("campaign_type", "conversion"))
        return build_phase4_prompt(
            task_id=task_id,
            image_paths=[str(p) for p in image_paths],
            campaign_type=campaign_type,
        )

    elif phase == 3.5:
        image_paths = kwargs.get("image_paths")
        if not image_paths:
            raise ValueError("Phase 3.5 requires 'image_paths' kwarg (non-empty list)")
        if not isinstance(image_paths, list):
            raise ValueError("Phase 3.5 'image_paths' must be a list of strings")
        cycle = int(str(kwargs.get("cycle", 1)))
        return build_phase3_5_prompt(
            task_id=task_id,
            image_paths=[str(p) for p in image_paths],
            cycle=cycle,
        )

    elif phase == 5:
        return build_phase5_prompt(task_id=task_id)

    else:
        raise ValueError(f"지원하지 않는 Phase 번호: {phase}. 허용 범위: -1, 0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5, 4, 5")


def build_workflow_overview_prompt(task_id: str) -> str:
    """dispatch.py --workflow image-qc-gate 용 워크플로우 개요 프롬프트 생성

    5Phase 워크플로우 개요 + QC 기준을 한 문자열로 반환하여
    task_desc 앞에 prepend 할 수 있도록 한다.

    Args:
        task_id: 작업 ID

    Returns:
        워크플로우 개요 프롬프트 문자열
    """
    sep = "=" * 60
    overview = (
        f"{sep}\n"
        "## 이미지 제작 QC 게이트 워크플��우 (v2.5)\n\n"
        "### ⚠️ 시작 조건\n"
        "- **제이회장님 승인 필수** — 디자인/이미지/광고 키워드가 있다고 자동 시작 금지\n"
        '- 아누는 제이회장님께 "이 작업에 이미지 QC 워크플로우를 적용하겠습니다" 보고 → 승인 후 시작\n'
        "- **한정승인 시**: 아누가 제이회장님 확인 없이 끝까지 orchestration\n\n"
        "### 워크플로우 개요\n"
        "- Phase -1: 노하우 프리로딩 — 과거 성공/실패 패턴 숙지\n"
        "- Phase 0: 브리프 검증 게이트 — 작업 브리프 유효성 검증\n"
        "- Phase 0.5: 브리프 QC — 5항목 100점 정량 검증\n"
        "- Phase 1: 카피/기획 (마케팅팀) — 광고 카피 및 기획안 작성\n"
        "- Phase 1.5: 기획 QC (마아트+로키) — 7항목 100점, PASS까지 반복\n"
        "- Phase 2: QC 검증 (카피) — 카피/기획안 품질 최종 검증\n"
        "- Phase 2.5: 파일럿 3종 제작 — 극단 케이스 자동 QC\n"
        "- Phase 3: 디자인 제작 (디자인팀) — 이미지 디자인 제작\n"
        "- Phase 3.5: 디자인 QC (로키 단독) — DQ 10항목 100점, PASS까지 반복\n"
        "- Phase 4: 최종 통합 검증 (3인 서명) — 카피+디자인 결합 최종 확인\n"
        "- Phase 5: 노하우 종합 검토 — 학습 기록 종합 + 교차 인사이트\n\n"
        "### QC 기준\n"
        "QC 기준(카테고리 A/B, FAIL 사유, 에스컬레이션)은 각 Phase에서 로딩됩니다.\n\n"
        f"Task ID: {task_id}\n"
        f"{sep}\n"
    )
    return overview


def check_escalation(fail_history: list[dict[str, object]], phase: str) -> dict[str, object]:
    """에스컬레이션 조건 체크

    이중 조건 체크:
    1. 동일 FAIL 카테고리 연속 반복 (same_fail_repeat 이상 시 에스컬레이션)
    2. QC 루프 상한 초과 (phase별 loop_max 이상 시 에스컬레이션)

    Args:
        fail_history: FAIL 이력 리스트. 각 항목은 dict:
            {
                "loop": int,           # 루프 회차 (1-based)
                "fail_code": str,      # FAIL_CATEGORIES 중 하나
                "phase": str,          # "copy" | "design"
                "detail": str,         # 선택적 상세 설명
            }
        phase: 현재 검사할 Phase 구분 ("copy" | "design")

    Returns:
        에스컬레이션 판정 결과 dict:
            {
                "escalate": bool,           # 에스컬레이션 필요 여부
                "reason": str,              # 에스컬레이션 사유 ("none" | "same_fail_repeat" | "loop_max_exceeded" | "both")
                "loop_count": int,          # 현재 루프 횟수
                "loop_max": int,            # 해당 Phase 루프 상한
                "repeated_fail_code": str,  # 반복된 FAIL 코드 ("none" 이면 해당 없음)
                "repeat_count": int,        # 해당 FAIL 코드 연속 반복 횟수
                "knowhow_path": str,        # 에스컬레이션 시 참조할 노하우 문서 경로
            }
    """
    # Phase별 루프 상한 결정
    if phase == "copy":
        loop_max = ESCALATION_RULES["copy_loop_max"]
    elif phase == "design":
        loop_max = ESCALATION_RULES["design_loop_max"]
    else:
        # 알 수 없는 phase는 보수적으로 copy 상한 적용
        loop_max = ESCALATION_RULES["copy_loop_max"]

    same_fail_threshold = ESCALATION_RULES["same_fail_repeat"]

    # 현재 Phase 이력만 필터링
    phase_history = [h for h in fail_history if h.get("phase") == phase]
    loop_count = len(phase_history)

    # 조건 1: 루프 상한 초과 여부
    loop_exceeded = loop_count >= loop_max

    # 조건 2: 동일 FAIL 코드 연속 반복 여부
    repeated_fail_code = "none"
    repeat_count = 0

    if len(phase_history) >= same_fail_threshold:
        # 최근 same_fail_threshold 개 이력에서 동일 코드 연속 확인
        recent = phase_history[-same_fail_threshold:]
        recent_codes = [str(h.get("fail_code", "")) for h in recent]
        if len(set(recent_codes)) == 1 and recent_codes[0] in FAIL_CATEGORIES:
            repeated_fail_code = recent_codes[0]
            # 전체 이력에서 해당 코드가 연속으로 몇 회 반복됐는지 계산
            repeat_count = 0
            for h in reversed(phase_history):
                if str(h.get("fail_code", "")) == repeated_fail_code:
                    repeat_count += 1
                else:
                    break

    same_fail_triggered = repeated_fail_code != "none"

    # 최종 reason 결정
    if loop_exceeded and same_fail_triggered:
        reason = "both"
    elif loop_exceeded:
        reason = "loop_max_exceeded"
    elif same_fail_triggered:
        reason = "same_fail_repeat"
    else:
        reason = "none"

    escalate = reason != "none"

    return {
        "escalate": escalate,
        "reason": reason,
        "loop_count": loop_count,
        "loop_max": loop_max,
        "repeated_fail_code": repeated_fail_code,
        "repeat_count": repeat_count,
        "knowhow_path": KNOWHOW_PATH,
    }
