"""사용자 디자인 PNG 10장 → Claude Sonnet Vision → Jinja card.html.j2 자동 생성.

⚠️ 실행 위치 — 로컬 권장:
    docker-compose의 insta-lab volume은 /app/data만 마운트. /app/app/templates는
    컨테이너 ephemeral이라 NAS docker exec로 돌리면 다음 rebuild에 결과물 소실됨.

로컬:
    cd insta-lab
    export ANTHROPIC_API_KEY=sk-ant-...
    python -m app.design_importer <theme> --templates-dir ./app/templates
    git add app/templates/<theme>/card.html.j2 && git commit + push

응급 hotfix만 NAS:
    docker exec insta-lab python -m app.design_importer <theme>
    docker cp insta-lab:/app/app/templates/<theme>/card.html.j2 ./<dst>
    # → 즉시 host repo에 commit + push (안 그러면 다음 rebuild에 소실)
"""

import base64
import datetime
import json
import logging
import re
from pathlib import Path
from typing import Any, Dict, List, Tuple

from anthropic import Anthropic
from jinja2 import BaseLoader, Environment, TemplateSyntaxError
from PIL import Image

from .config import ANTHROPIC_API_KEY, ANTHROPIC_MODEL_SONNET

logger = logging.getLogger(__name__)

__all__ = [
    "_resolve_page_mapping",
    "_validate_images",
    "_call_vision",
    "_validate_html_template",
    "import_design_theme",
]

# 페이지 1 (커버) 키워드 우선순위 — 먼저 매치된 키워드를 가진 첫 파일만 page 1
_COVER_KEYWORDS = ("cover", "start", "intro")
# 페이지 10 (CTA) 키워드 우선순위
_CTA_KEYWORDS = ("cta", "outro", "finish", "end")

# 인스타그램 카드 규격 (세로형 4:5 비율)
_EXPECTED_SIZE = (1080, 1350)


def _resolve_page_mapping(pages_dir: Path) -> Dict[str, int]:
    """templates/<theme>/pages/ 안의 PNG 10장을 page 1~10에 매핑.

    우선순위:
    1. `_order.json` 있으면 그 매핑 그대로 사용 (검증 통과 시 반환)
    2. 자동 매핑:
       - _COVER_KEYWORDS 우선순위 순서로 가장 앞 키워드를 가진 첫 PNG → page 1
       - _CTA_KEYWORDS 우선순위 순서로 가장 앞 키워드를 가진 첫 PNG → page 10
       - 남은 8장은 알파벳 정렬 → page 2~9
    """
    pages_dir = Path(pages_dir)
    pngs = sorted([p.name for p in pages_dir.glob("*.png")])
    if len(pngs) != 10:
        raise ValueError(
            f"{pages_dir}에 PNG 10장 필요, 발견 {len(pngs)}장: {pngs}"
        )

    order_path = pages_dir / "_order.json"
    if order_path.exists():
        try:
            mapping = json.loads(order_path.read_text(encoding="utf-8"))
        except Exception as e:
            logger.warning("_order.json 파싱 실패, 자동 매핑으로 폴백: %s", e)
        else:
            if set(mapping.keys()) == set(pngs) and set(mapping.values()) == set(range(1, 11)):
                return {k: int(v) for k, v in mapping.items()}
            logger.warning(
                "_order.json 형식 오류 (파일 누락·page 중복), 자동 매핑으로 폴백"
            )

    return _build_mapping(pngs)


def _pick_by_keywords(names: List[str], keywords: tuple) -> str | None:
    """names 중 keywords의 우선순위에 따라 첫 매치 파일명 반환 (없으면 None)."""
    lower_names = [(n, n.lower()) for n in names]
    for kw in keywords:
        for orig, low in lower_names:
            if kw in low:
                return orig
    return None


def _build_mapping(pngs: List[str]) -> Dict[str, int]:
    """자동 매핑 알고리즘 본체."""
    mapping: Dict[str, int] = {}
    remaining = list(pngs)

    cover = _pick_by_keywords(remaining, _COVER_KEYWORDS)
    if cover:
        mapping[cover] = 1
        remaining.remove(cover)

    cta = _pick_by_keywords(remaining, _CTA_KEYWORDS)
    if cta:
        mapping[cta] = 10
        remaining.remove(cta)

    remaining_sorted = sorted(remaining)
    free_pages = sorted(set(range(1, 11)) - set(mapping.values()))
    for name, page in zip(remaining_sorted, free_pages):
        mapping[name] = page

    return mapping


_EXPECTED_RATIO = 1080 / 1350  # 4:5 = 0.8
_RATIO_TOLERANCE = 0.02         # ±2% (1122/1402 ≈ 0.80028도 통과)


def _validate_images(pages_dir: Path) -> None:
    """모든 PNG가 4:5 종횡비(1080x1350 권장)에 가까운지 검증.

    Vision은 base64로 원본을 분석하고 Playwright는 background-size: cover로
    1080x1350 컨테이너에 fit하므로 절대 사이즈는 유연. 단 종횡비가 어긋나면
    카드가 늘어나거나 잘리므로 ±2% 허용 범위 내에서만 통과.

    early-exit 하지 않고 전체 파일을 검사한 뒤 한 메시지에 모아 raise.
    """
    pages_dir = Path(pages_dir)
    bad = []
    for png_path in sorted(pages_dir.glob("*.png")):
        with Image.open(png_path) as img:
            w, h = img.size
            if h == 0:
                bad.append((png_path.name, img.size))
                continue
            ratio = w / h
            if abs(ratio - _EXPECTED_RATIO) > _RATIO_TOLERANCE:
                bad.append((png_path.name, img.size))
    if bad:
        msg = "; ".join(f"{n}: {s[0]}x{s[1]}" for n, s in bad)
        raise ValueError(
            f"카드 디자인은 4:5 비율(1080x1350 권장)이어야 함. 잘못된 파일: {msg}"
        )


# ── Vision 호출 + HTML 생성 ───────────────────────────────────────────────────

_VISION_SYSTEM_PROMPT = """너는 인스타그램 카드 뉴스 디자인을 모방하는 프론트엔드 디자이너다.

입력: 10장의 카드 디자인 이미지 (각 1080×1350, placeholder 텍스트가 박혀있음) + 파일명 → 페이지 번호 매핑.
출력: 단일 Jinja2 HTML 파일 본문 (코드펜스·설명 텍스트 금지).

핵심 제약 — placeholder 텍스트 마스킹:
PNG에는 디자인 placeholder 텍스트가 이미 그려져 있다. 동적 카피로 교체할 때
원본 텍스트가 비치면 안 된다. 각 텍스트 영역마다 두 layer를 그려라:
  (a) 마스킹 박스: position: absolute로 placeholder 영역과 같은 좌표.
      background는 그 영역 주변 픽셀 색 (카드 배경색)에서 추출. padding 8px 여유.
  (b) 동적 텍스트 layer: 마스킹 박스와 동일 좌표.
      font-size·font-weight·color는 원본 placeholder의 스타일을 모방.
      {{ headline }} / {{ body }} / {{ cta }} Jinja 변수 사용.

페이지 종류별 영역 가이드:
- page 1 (cover): 메인 headline 1개 영역
- page 2~9 (body): headline 영역 + body 영역
- page 10 (cta): headline + body + cta 영역

요구사항:
- 컨테이너 width 1080px, height 1350px
- 각 페이지마다 `background-image: url('pages/{{filename}}')`로 사용자 PNG 로드
- page_no 1~10 분기: {% if page_no == N %}...{% endif %} 구조
- 폰트는 Noto Sans KR (Google Fonts CDN). letter-spacing -0.02em, line-height 1.3 기본
- 텍스트 영역은 word-wrap: break-word + overflow: hidden (동적 카피가 길어도 마스킹 박스 밖으로 안 새도록)
- HTML <head>에 <style>로 모든 CSS 인라인. <link> 외부 stylesheet 금지
- 출력은 <!DOCTYPE html>로 시작하는 완전한 HTML 문서
"""


def _call_vision(images_with_pages: List[Tuple[str, int, bytes]],
                 theme_name: str) -> Dict[str, Any]:
    """Claude Sonnet Vision 호출. images_with_pages: [(filename, page_no, png_bytes), ...].

    Returns: {"html": str, "tokens": int, "summary": str}
    """
    if not ANTHROPIC_API_KEY:
        raise RuntimeError("ANTHROPIC_API_KEY 미설정 — design_importer 사용 불가")

    client = Anthropic(api_key=ANTHROPIC_API_KEY)
    content: List[Dict[str, Any]] = []
    for filename, page_no, png_bytes in sorted(images_with_pages, key=lambda x: x[1]):
        content.append({
            "type": "image",
            "source": {
                "type": "base64",
                "media_type": "image/png",
                "data": base64.b64encode(png_bytes).decode("ascii"),
            },
        })
        content.append({
            "type": "text",
            "text": f"위 이미지 = '{filename}' = page {page_no}",
        })
    content.append({
        "type": "text",
        "text": (
            f"theme 이름: '{theme_name}'. 위 10장 디자인을 모방한 단일 Jinja2 HTML을 출력해라."
        ),
    })

    msg = client.messages.create(
        model=ANTHROPIC_MODEL_SONNET,
        max_tokens=16000,
        system=_VISION_SYSTEM_PROMPT,
        messages=[{"role": "user", "content": content}],
    )
    raw = msg.content[0].text.strip()
    # 코드펜스 자르기
    if raw.startswith("```"):
        raw = re.sub(r"^```(?:html)?\s*|\s*```$", "", raw).strip()
    summary = raw[:200].replace("\n", " ")  # 첫 200자만 분석 요약으로
    return {
        "html": raw,
        "tokens": msg.usage.input_tokens + msg.usage.output_tokens,
        "summary": summary,
    }


def _validate_html_template(html: str) -> None:
    """Jinja2 Environment로 sanity render. 문법 오류면 TemplateSyntaxError 전파."""
    env = Environment(loader=BaseLoader())
    env.from_string(html)  # 파싱만으로도 syntax error 검출


def import_design_theme(theme_dir: str) -> Dict[str, Any]:
    """templates/<theme>/pages/*.png 10장 → Vision → card.html.j2 생성.

    Args:
        theme_dir: theme 디렉토리 절대 경로 (예: /app/app/templates/minimal)
    Returns:
        {theme_name, html_path, page_mapping, analysis_summary, tokens_used}
    """
    theme_path = Path(theme_dir)
    theme_name = theme_path.name
    pages_dir = theme_path / "pages"

    # 1. 매핑 + 검증
    mapping = _resolve_page_mapping(pages_dir)
    _validate_images(pages_dir)

    # 2. Vision 호출
    images_with_pages = []
    for filename, page_no in mapping.items():
        png_bytes = (pages_dir / filename).read_bytes()
        images_with_pages.append((filename, page_no, png_bytes))

    vision_result = _call_vision(images_with_pages, theme_name)
    html = vision_result["html"]

    # 3. Jinja sanity
    html_path = theme_path / "card.html.j2"
    try:
        _validate_html_template(html)
    except TemplateSyntaxError as e:
        error_path = theme_path / "card.html.j2.error.txt"
        error_path.write_text(html, encoding="utf-8")
        raise ValueError(
            f"Vision 응답이 Jinja 문법 오류: {e}. 원본 HTML은 {error_path}에 저장됨"
        )

    # 4. 백업 + 저장
    if html_path.exists():
        ts = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
        backup_path = theme_path / f"card.html.j2.bak.{ts}"
        html_path.rename(backup_path)
        logger.info("기존 HTML 백업: %s", backup_path)

    html_path.write_text(html, encoding="utf-8")

    return {
        "theme_name": theme_name,
        "html_path": str(html_path),
        "page_mapping": mapping,
        "analysis_summary": vision_result["summary"],
        "tokens_used": vision_result["tokens"],
    }


# ── CLI entrypoint ───────────────────────────────────────────────────────────

def main_cli():
    """CLI: python -m app.design_importer <theme_name> [--templates-dir PATH]"""
    import argparse
    parser = argparse.ArgumentParser(
        prog="design_importer",
        description="사용자 카드 디자인 PNG 10장을 Claude Vision으로 분석해 card.html.j2 생성",
    )
    parser.add_argument("theme_name", help="templates/<theme_name>/ 디렉토리명")
    parser.add_argument(
        "--templates-dir",
        default="/app/app/templates",
        help="templates 루트 디렉토리 (기본 컨테이너 내부 경로)",
    )
    args = parser.parse_args()

    theme_dir = Path(args.templates_dir) / args.theme_name
    if not theme_dir.is_dir():
        print(f"ERROR: theme 디렉토리 없음: {theme_dir}")
        raise SystemExit(1)

    try:
        result = import_design_theme(str(theme_dir))
    except Exception as e:
        print(f"ERROR: {e}")
        raise SystemExit(1)

    print(json.dumps(result, ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main_cli()
