"""개선 제안 체크리스트 웹 서버 — LAN 접속 가능."""
import json
import http.server
import socketserver
from pathlib import Path

PORT = 9090
RESULT_FILE = Path(__file__).parent / "checklist-result.json"

# 초기 결과 로드 or 생성
if RESULT_FILE.exists():
    decisions = json.loads(RESULT_FILE.read_text())
else:
    decisions = {}

ITEMS = [
    # (번호, 카테고리, 제안, effort, impact)
    # 1. AI 분석 품질
    (1, "AI 분석 품질", "씬 타입 분류 (몽타주/회상/꿈/V.O.) → 타입별 T2I 전략", "S", "high"),
    (2, "AI 분석 품질", "씬 분석 피드백 루프 — GPT 교차 검증", "M", "high"),
    (3, "AI 분석 품질", "참조 이미지 간 일관성 검증 — '같은 인물인가?' GPT Vision", "L", "high"),
    (4, "AI 분석 품질", "프롬프트 번역 역번역 검증 — 한→영→한 의미 비교", "M", "medium"),
    (5, "AI 분석 품질", "adaptive 씬 변형 수 — 단순 씬 2개, 액션 씬 5개", "M", "medium"),
    (6, "AI 분석 품질", "다중 에피소드 학습 — 이전 에피소드 확정 요소 컨텍스트 전달", "M", "high"),
    (7, "AI 분석 품질", "엔티티 설명 잘림 40자→150자 확대", "S", "medium"),
    (8, "AI 분석 품질", "WorldGuide style_rules 필드 추가 — 씬 가드레일 실제 적용", "M", "high"),
    (9, "AI 분석 품질", "GPT 엔티티 리뷰(Turn 1.5) 결과 실제 활용 or 제거", "S", "medium"),
    (10, "AI 분석 품질", "씬 의존성 텍스트 미리보기 200→600자 확대", "S", "medium"),
    # 2. 이미지 생성 품질
    (11, "이미지 품질", "T2I 변형 구도 다양성 강제 — camera_angle_hint 필수", "M", "high"),
    (12, "이미지 품질", "첫 등장 장소에 location 참조 이미지 포함", "M", "high"),
    (13, "이미지 품질", "씬 간 캐릭터 외모 일관성 교차 검증 + 재생성", "L", "high"),
    (14, "이미지 품질", "composite 아웃룩 이미지 — 구조화 프롬프트 빌더 경유", "M", "high"),
    (15, "이미지 품질", "다중 신호 이미지 랭킹 — 검증+연속성+커버리지 복합 스코어", "M", "high"),
    (16, "이미지 품질", "이미지 검증 threshold 엔티티 타입별 커스텀", "S", "medium"),
    (17, "이미지 품질", "next_ref(뒤쪽 연관 씬) 저장 + 양방향 연속성", "S", "medium"),
    (18, "이미지 품질", "토너먼트 선택 프롬프트 외부화", "S", "low"),
    # 3. 성능
    (19, "성능", "DB 인덱스 추가 — 핵심 10개 쿼리 컬럼", "S", "critical"),
    (20, "성능", "N+1 쿼리 제거 — list_stills 275개→IN 1개", "M", "high"),
    (21, "성능", "DB 커넥션 풀 설정 (pool_size=10, max_overflow=20)", "S", "high"),
    (22, "성능", "Episode.fulltext lazy loading (defer)", "S", "medium"),
    (23, "성능", "씬 변형 내부 병렬 생성 — 씬당 ~2배 단축", "M", "high"),
    (24, "성능", "엔티티 워커 스태거 적응형 (2초→0.5초+backoff)", "S", "medium"),
    (25, "성능", "progress 5개 쿼리 → IN 1개 통합", "S", "medium"),
    (26, "성능", "참조 이미지 바이트 lazy loading", "S", "low"),
    # 4. UX
    (27, "UX", "글로벌 토스트/에러 시스템", "M", "critical"),
    (28, "UX", "EpisodeDetail 2패널 레이아웃 (씬 목록 + 상세)", "L", "high"),
    (29, "UX", "SceneVariationCard 아코디언 3단 접기", "M", "high"),
    (30, "UX", "이미지 리뷰 키보드 단축키 ←→ A R S", "M", "high"),
    (31, "UX", "배치 승인/거부 — 체크박스 다중선택", "M", "high"),
    (32, "UX", "이미지 나란히 비교 뷰", "M", "high"),
    (33, "UX", "SSE 실시간 업데이트 (폴링 대체)", "L", "high"),
    (34, "UX", "WorldGuide 구조화 폼 에디터", "M", "high"),
    (35, "UX", "씬 점프 네비게이션 + G 단축키", "S", "high"),
    (36, "UX", "엔티티 텍스트 검색", "S", "high"),
    (37, "UX", "스켈레톤 로더", "S", "medium"),
    (38, "UX", "세그먼테이션 슬라이더 디바운스", "S", "medium"),
    (39, "UX", "에피소드 번호 자동 증가", "S", "low"),
    (40, "UX", "이미지 리뷰 필터 카운트 뱃지", "S", "low"),
    (41, "UX", "이미지 리뷰 카드에 씬 컨텍스트", "S", "medium"),
    (42, "UX", "Export 설정 툴팁 + 에피소드 라벨", "S", "low"),
    # 5. 아키텍처
    (43, "아키텍처", "AnalysisService God Method 분할 (3개 메서드)", "M", "medium"),
    (44, "아키텍처", "ImageService 분할 (4개 서비스)", "L", "medium"),
    (45, "아키텍처", "백그라운드 태스크 레지스트리 + stale 복구", "M", "high"),
    (46, "아키텍처", "Alembic 마이그레이션 도입", "M", "medium"),
    (47, "아키텍처", "구조화 로깅 (structlog)", "M", "medium"),
    (48, "아키텍처", "헬스체크 엔드포인트", "S", "medium"),
    (49, "아키텍처", "LLMCallLog 커버리지 확대 (4건 urllib→OpenAIClient)", "M", "medium"),
    (50, "아키텍처", "데이터 정규화 — visible_entities JSON→별도 테이블", "L", "critical"),
    (51, "아키텍처", "재분석 시 고아 ImageAsset 정리", "S", "medium"),
    (52, "아키텍처", "순차 모드(v3) 데드코드 120줄 제거", "S", "low"),
    # 6. 보안
    (53, "보안", "기본 비밀번호 제거 — .env 필수", "S", "critical"),
    (54, "보안", "세션 쿠키 Secure 플래그", "S", "high"),
    (55, "보안", "LLMCallLog 프롬프트 자르기 10K자 + 30일 보존", "S", "medium"),
    (56, "보안", "PostgreSQL 통합 테스트 환경", "M", "high"),
    # 7. 전략적
    (57, "전략", "내구성 작업 큐 (Celery/ARQ)", "XL", "critical"),
    (58, "전략", "증분 파이프라인 — content hash 기반 선택적 재실행", "XL", "critical"),
    (59, "전략", "씬 플래닝 아티팩트 — n-way 분할 + 사용자 승인", "M", "high"),
    (60, "전략", "WorldGuide 버전 관리 — 소스 해시 + 부분 갱신", "M", "high"),
    (61, "전략", "다중 사용자 작업 잠금 — exclusive lease", "M", "high"),
    (62, "전략", "운영 대시보드 — traces UI + 비용 메트릭", "M", "high"),
    (63, "전략", "프론트엔드 데이터 레이어 (typed query + cache)", "M", "medium"),
    (64, "전략", "E2E 파이프라인 테스트 — fixture LLM 응답", "M", "medium"),
]

IMPACT_COLORS = {
    "critical": "#ef4444",
    "high": "#f59e0b",
    "medium": "#3b82f6",
    "low": "#6b7280",
}

EFFORT_LABELS = {"S": "🟢 S", "M": "🟡 M", "L": "🟠 L", "XL": "🔴 XL"}

def build_html():
    rows = []
    current_cat = ""
    for num, cat, desc, effort, impact in ITEMS:
        if cat != current_cat:
            current_cat = cat
            rows.append(f'<tr class="cat-header"><td colspan="6"><strong>{cat}</strong></td></tr>')
        d = decisions.get(str(num), "")
        y_cls = "btn-y active" if d == "Y" else "btn-y"
        n_cls = "btn-n active" if d == "N" else "btn-n"
        ic = IMPACT_COLORS.get(impact, "#999")
        ef = EFFORT_LABELS.get(effort, effort)
        rows.append(f'''<tr data-num="{num}">
  <td class="num">{num}</td>
  <td class="desc">{desc}</td>
  <td class="effort">{ef}</td>
  <td class="impact"><span style="background:{ic}">{impact}</span></td>
  <td class="decision">
    <button class="{y_cls}" onclick="decide({num},'Y')">Y</button>
    <button class="{n_cls}" onclick="decide({num},'N')">N</button>
  </td>
  <td class="status" id="s{num}">{d}</td>
</tr>''')

    table = "\n".join(rows)
    y_count = sum(1 for v in decisions.values() if v == "Y")
    n_count = sum(1 for v in decisions.values() if v == "N")
    remaining = len(ITEMS) - y_count - n_count

    return f'''<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TheRoad — 개선 제안 체크리스트</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  background: #0f172a; color: #e2e8f0; padding: 20px; }}
h1 {{ text-align: center; margin-bottom: 8px; font-size: 24px; }}
.summary {{ text-align: center; margin-bottom: 20px; font-size: 14px; color: #94a3b8; }}
.summary span {{ padding: 4px 12px; border-radius: 12px; margin: 0 4px; font-weight: 600; }}
.s-y {{ background: #166534; color: #4ade80; }}
.s-n {{ background: #7f1d1d; color: #fca5a5; }}
.s-r {{ background: #1e3a5f; color: #93c5fd; }}
table {{ width: 100%; border-collapse: collapse; }}
th {{ background: #1e293b; padding: 10px 8px; text-align: left; font-size: 13px;
  position: sticky; top: 0; z-index: 10; }}
td {{ padding: 8px; border-bottom: 1px solid #1e293b; font-size: 13px; }}
tr:hover {{ background: #1e293b; }}
.cat-header td {{ background: #172554; padding: 12px 8px; font-size: 14px;
  border-bottom: 2px solid #1e40af; }}
.num {{ width: 40px; text-align: center; color: #64748b; font-weight: 700; }}
.desc {{ max-width: 500px; }}
.effort {{ width: 60px; text-align: center; }}
.impact span {{ padding: 2px 8px; border-radius: 8px; color: white; font-size: 11px;
  font-weight: 600; }}
.decision {{ width: 100px; text-align: center; }}
.decision button {{ border: none; padding: 6px 14px; border-radius: 6px; cursor: pointer;
  font-weight: 700; font-size: 13px; margin: 0 2px; transition: all 0.15s; }}
.btn-y {{ background: #1e293b; color: #4ade80; }}
.btn-y:hover {{ background: #166534; }}
.btn-y.active {{ background: #166534; color: white; box-shadow: 0 0 8px #4ade80; }}
.btn-n {{ background: #1e293b; color: #f87171; }}
.btn-n:hover {{ background: #7f1d1d; }}
.btn-n.active {{ background: #7f1d1d; color: white; box-shadow: 0 0 8px #f87171; }}
.status {{ width: 30px; text-align: center; font-weight: 700; }}
.toolbar {{ position: fixed; bottom: 0; left: 0; right: 0; background: #1e293b;
  padding: 12px 20px; display: flex; justify-content: space-between; align-items: center;
  border-top: 2px solid #334155; z-index: 20; }}
.toolbar button {{ padding: 10px 24px; border-radius: 8px; border: none; font-weight: 700;
  font-size: 14px; cursor: pointer; }}
.btn-submit {{ background: #2563eb; color: white; }}
.btn-submit:hover {{ background: #1d4ed8; }}
.btn-all-y {{ background: #166534; color: white; }}
.btn-all-n {{ background: #7f1d1d; color: white; }}
.btn-reset {{ background: #334155; color: #94a3b8; }}
.spacer {{ height: 60px; }}
</style>
</head>
<body>
<h1>TheRoad Scene Lab — 개선 제안 체크리스트</h1>
<div class="summary">
  <span class="s-y">적용 {y_count}</span>
  <span class="s-n">보류 {n_count}</span>
  <span class="s-r">미결정 {remaining}</span>
  / 총 {len(ITEMS)}건
</div>
<table>
<thead><tr>
  <th>#</th><th>제안</th><th>Effort</th><th>Impact</th><th>결정</th><th></th>
</tr></thead>
<tbody>
{table}
</tbody>
</table>
<div class="spacer"></div>
<div class="toolbar">
  <div>
    <button class="btn-all-y" onclick="bulkDecide('Y')">미결정 전체 Y</button>
    <button class="btn-all-n" onclick="bulkDecide('N')">미결정 전체 N</button>
    <button class="btn-reset" onclick="resetAll()">초기화</button>
  </div>
  <button class="btn-submit" onclick="submitAll()">제출 완료</button>
</div>
<script>
let data = {json.dumps(decisions)};

function decide(num, val) {{
  data[num] = val;
  // Update buttons
  const tr = document.querySelector(`tr[data-num="${{num}}"]`);
  if (!tr) return;
  tr.querySelectorAll('.btn-y').forEach(b => b.classList.toggle('active', val==='Y'));
  tr.querySelectorAll('.btn-n').forEach(b => b.classList.toggle('active', val==='N'));
  document.getElementById('s'+num).textContent = val;
  // Auto-save
  fetch('/save', {{method:'POST', headers:{{'Content-Type':'application/json'}},
    body:JSON.stringify({{num:num, val:val}})}});
  updateSummary();
}}

function bulkDecide(val) {{
  document.querySelectorAll('tr[data-num]').forEach(tr => {{
    const num = tr.dataset.num;
    if (!data[num]) decide(parseInt(num), val);
  }});
}}

function resetAll() {{
  if (!confirm('모든 결정을 초기화합니까?')) return;
  data = {{}};
  fetch('/reset', {{method:'POST'}});
  location.reload();
}}

function submitAll() {{
  fetch('/submit', {{method:'POST', headers:{{'Content-Type':'application/json'}},
    body:JSON.stringify(data)}}).then(r => r.json()).then(d => {{
    alert(`제출 완료!\\n\\n적용: ${{d.y}}건\\n보류: ${{d.n}}건\\n미결정: ${{d.remaining}}건`);
  }});
}}

function updateSummary() {{
  const y = Object.values(data).filter(v=>v==='Y').length;
  const n = Object.values(data).filter(v=>v==='N').length;
  const r = {len(ITEMS)} - y - n;
  document.querySelector('.summary').innerHTML =
    `<span class="s-y">적용 ${{y}}</span><span class="s-n">보류 ${{n}}</span><span class="s-r">미결정 ${{r}}</span> / 총 {len(ITEMS)}건`;
}}
</script>
</body>
</html>'''


class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.end_headers()
        self.wfile.write(build_html().encode("utf-8"))

    def do_POST(self):
        length = int(self.headers.get("Content-Length", 0))
        body = self.rfile.read(length) if length else b""

        if self.path == "/save":
            d = json.loads(body)
            decisions[str(d["num"])] = d["val"]
            RESULT_FILE.write_text(json.dumps(decisions, indent=2))
            self._json_ok({"ok": True})

        elif self.path == "/reset":
            decisions.clear()
            RESULT_FILE.write_text("{}")
            self._json_ok({"ok": True})

        elif self.path == "/submit":
            d = json.loads(body)
            decisions.update({str(k): v for k, v in d.items()})
            RESULT_FILE.write_text(json.dumps(decisions, indent=2))
            y = sum(1 for v in decisions.values() if v == "Y")
            n = sum(1 for v in decisions.values() if v == "N")
            self._json_ok({"y": y, "n": n, "remaining": len(ITEMS) - y - n})
        else:
            self._json_ok({"error": "not found"}, 404)

    def _json_ok(self, data, code=200):
        self.send_response(code)
        self.send_header("Content-Type", "application/json")
        self.end_headers()
        self.wfile.write(json.dumps(data).encode("utf-8"))

    def log_message(self, format, *args):
        pass  # suppress logs


with socketserver.TCPServer(("0.0.0.0", PORT), Handler) as httpd:
    import socket
    ip = socket.gethostbyname(socket.gethostname())
    print(f"\n  체크리스트 서버 실행 중:")
    print(f"  http://localhost:{PORT}")
    print(f"  http://{ip}:{PORT}")
    print(f"\n  LAN에서 접속 가능합니다. Ctrl+C로 종료.\n")
    httpd.serve_forever()
