# -*- coding: utf-8 -*-
"""
STEP111-02 HBE Layout Composer v1

목적:
- 운영 초안 생성 코드는 건드리지 않는다.
- HBE Story Planner 결과를 바탕으로 사람형 블로그 레이아웃 후보를 여러 개 생성한다.
- 고정 템플릿이 아니라 "블록 조립" 방식으로 다양한 레이아웃을 만든다.
- 필수 블록은 절대 누락하지 않는다.

사용:
  cd /d D:\honghee\blog_api

  draft 기준:
  python tools\hbe_layout_composer.py --draft-id 209

  article 기준:
  python tools\hbe_layout_composer.py --realtor-id 1 --article-no 2633019770

  후보 수:
  python tools\hbe_layout_composer.py --draft-id 209 --count 5

  JSON:
  python tools\hbe_layout_composer.py --draft-id 209 --json
"""

import argparse
import json
import random
import re
import sys
from pathlib import Path
from html import unescape

ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))

from db import get_conn


REQUIRED_BLOCKS = [
    "representative_image",
    "realtor_map",
    "realtor_info",
    "legal_disclosure",
    "seo_footer",
]

CORE_BLOCKS = [
    "human_intro_from_broker",
    "key_info_table",
    "broker_description",
    "floorplan",
    "photo_story",
    "location_photo_story",
    "middle_banner",
    "community_or_school_note",
    "price_condition_note",
    "checklist",
    "transport_living_note",
    "space_commentary",
    "location_note",
]


def clean_text(value):
    value = "" if value is None else str(value)
    value = unescape(value)
    value = re.sub(r"<[^>]+>", " ", value)
    value = re.sub(r"\s+", " ", value)
    return value.strip()


def safe_json_loads(value):
    if not value:
        return {}
    if isinstance(value, dict):
        return value
    try:
        return json.loads(value)
    except Exception:
        return {}


def fetch_draft(conn, draft_id=None, realtor_id=None, article_no=None):
    with conn.cursor() as cur:
        if draft_id:
            cur.execute("SELECT * FROM blog_article_drafts WHERE id=%s LIMIT 1", (int(draft_id),))
            return cur.fetchone()

        cur.execute(
            """
            SELECT *
            FROM blog_article_drafts
            WHERE realtor_id=%s AND article_no=%s
            ORDER BY id DESC
            LIMIT 1
            """,
            (int(realtor_id), str(article_no)),
        )
        return cur.fetchone()


def count_images_by_hint(html):
    html = html or ""
    return {
        "total": len(re.findall(r"<img\b", html, flags=re.I)),
        "floorplan": len(re.findall(r"floorplan|평면도|photoinfra", html, flags=re.I)),
        "realtor_map": len(re.findall(r"realtor_naver_map_|realestate-realtor-map-image", html, flags=re.I)),
        "location": len(re.findall(r"입지|주변환경|land_naver", html, flags=re.I)),
        "header": len(re.findall(r"realestate-representative-image|header_", html, flags=re.I)),
    }


def extract_source_json(draft):
    return safe_json_loads(draft.get("source_json")) or {"detail": {}, "images": [], "schools": [], "prices": []}


def analyze_signals(draft):
    html = draft.get("clipboard_html") or draft.get("draft_html") or ""
    text = clean_text(html)
    source = extract_source_json(draft)
    detail = source.get("detail") or {}
    schools = source.get("schools") or []
    prices = source.get("prices") or []

    raw_json = safe_json_loads(detail.get("raw_json"))
    article_detail = raw_json.get("articleDetail") or {}

    broker_desc = clean_text(
        detail.get("article_feature_desc")
        or article_detail.get("detailDescription")
        or article_detail.get("articleFeatureDescription")
        or ""
    )

    combined = " ".join([
        clean_text(draft.get("draft_title")),
        clean_text(detail.get("article_name")),
        clean_text(detail.get("article_feature_desc")),
        broker_desc,
        text[:5000],
    ])

    image_counts = count_images_by_hint(html)

    keyword_groups = {
        "school": ["초", "중", "고", "학교", "학군", "통학", "배정학교"],
        "family": ["대단지", "세대", "커뮤니티", "도서관", "게스트하우스", "스포츠센터", "놀이터"],
        "transport": ["역", "지하철", "BRT", "brt", "정류장", "교통", "버스", "IC", "대로"],
        "newly": ["신축", "4년이내", "준신축", "신축급", "깨끗"],
        "option": ["풀옵션", "옵션", "냉장고", "세탁기", "에어컨", "인덕션", "붙박이"],
        "view_light": ["채광", "조망", "남향", "남동향", "남서향", "뷰"],
        "price": ["급매", "가격", "저렴", "시세", "투자", "수익", "전세가"],
        "movein": ["입주", "즉시", "협의", "빠른입주"],
    }

    scores = {}
    lower_combined = combined.lower()
    for group, words in keyword_groups.items():
        hits = [word for word in words if word.lower() in lower_combined]
        scores[group] = {"score": len(hits), "hits": hits}

    scores["location_photo"] = {"score": 3 if image_counts["location"] >= 4 else 0, "hits": ["입지이미지多"] if image_counts["location"] >= 4 else []}
    scores["structure"] = {"score": 2 if image_counts["floorplan"] >= 1 else 0, "hits": ["평면도"] if image_counts["floorplan"] >= 1 else []}

    if len(schools) > 0:
        scores["school"]["score"] += 2
        scores["school"]["hits"].append("schools_table")

    if len(prices) > 0:
        scores["price"]["score"] += 1
        scores["price"]["hits"].append("prices_table")

    ranked = sorted(scores.items(), key=lambda kv: kv[1].get("score", 0), reverse=True)
    top_signal = ranked[0][0] if ranked and ranked[0][1].get("score", 0) > 0 else "general"

    return {
        "draft_id": draft.get("id"),
        "realtor_id": draft.get("realtor_id"),
        "article_no": draft.get("article_no"),
        "title": draft.get("draft_title"),
        "broker_desc_len": len(broker_desc),
        "image_counts": image_counts,
        "scores": scores,
        "ranked_signals": [
            {"key": k, "score": v.get("score", 0), "hits": v.get("hits", [])}
            for k, v in ranked[:6]
        ],
        "top_signal": top_signal,
    }


def story_profile_from_signal(signal):
    mapping = {
        "school": "family_school_story",
        "family": "large_complex_living_story",
        "transport": "transport_access_story",
        "newly": "condition_clean_story",
        "option": "option_practical_story",
        "view_light": "light_view_story",
        "price": "price_condition_story",
        "location_photo": "photo_location_story",
        "structure": "structure_floorplan_story",
    }
    return mapping.get(signal, "balanced_human_story")


def available_blocks(signals, story_profile):
    image_counts = signals["image_counts"]
    broker_len = signals["broker_desc_len"]

    blocks = set(REQUIRED_BLOCKS)
    blocks.update(["human_intro_from_broker", "key_info_table", "middle_banner"])

    if broker_len > 0:
        blocks.add("broker_description")

    if image_counts["floorplan"] > 0:
        blocks.add("floorplan")
        blocks.add("space_commentary")

    if image_counts["total"] >= 6:
        blocks.add("photo_story")

    if image_counts["location"] >= 4:
        blocks.add("location_photo_story")
        blocks.add("location_note")

    if story_profile in ["family_school_story", "large_complex_living_story"]:
        blocks.add("community_or_school_note")

    if story_profile in ["transport_access_story", "photo_location_story"]:
        blocks.add("transport_living_note")

    if story_profile == "price_condition_story":
        blocks.add("price_condition_note")
        blocks.add("checklist")

    if story_profile in ["structure_floorplan_story", "option_practical_story"]:
        blocks.add("space_commentary")
        blocks.add("checklist")

    return list(blocks)


def compose_candidate(signals, story_profile, variant_index):
    blocks = available_blocks(signals, story_profile)

    # 필수 블록 보호
    for rb in REQUIRED_BLOCKS:
        if rb not in blocks:
            blocks.append(rb)

    # 사람형 흐름 템플릿: 고정이 아니라 프로필별 흐름 후보
    base_flows = {
        "family_school_story": [
            ["representative_image", "human_intro_from_broker", "location_photo_story", "community_or_school_note", "broker_description", "key_info_table", "floorplan", "photo_story", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
            ["representative_image", "human_intro_from_broker", "broker_description", "photo_story", "key_info_table", "floorplan", "community_or_school_note", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
            ["representative_image", "photo_story", "human_intro_from_broker", "community_or_school_note", "key_info_table", "floorplan", "broker_description", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
        ],
        "large_complex_living_story": [
            ["representative_image", "human_intro_from_broker", "community_or_school_note", "location_photo_story", "broker_description", "key_info_table", "photo_story", "floorplan", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
            ["representative_image", "location_photo_story", "human_intro_from_broker", "photo_story", "broker_description", "key_info_table", "floorplan", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
        ],
        "photo_location_story": [
            ["representative_image", "location_photo_story", "human_intro_from_broker", "transport_living_note", "key_info_table", "floorplan", "broker_description", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
            ["representative_image", "human_intro_from_broker", "location_photo_story", "photo_story", "key_info_table", "broker_description", "floorplan", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
        ],
        "structure_floorplan_story": [
            ["representative_image", "human_intro_from_broker", "key_info_table", "floorplan", "space_commentary", "photo_story", "broker_description", "checklist", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
            ["representative_image", "photo_story", "human_intro_from_broker", "floorplan", "key_info_table", "broker_description", "space_commentary", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
        ],
        "price_condition_story": [
            ["representative_image", "key_info_table", "price_condition_note", "photo_story", "floorplan", "broker_description", "checklist", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
            ["representative_image", "human_intro_from_broker", "key_info_table", "broker_description", "price_condition_note", "floorplan", "photo_story", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
        ],
        "balanced_human_story": [
            ["representative_image", "human_intro_from_broker", "key_info_table", "photo_story", "floorplan", "broker_description", "location_note", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
            ["representative_image", "photo_story", "human_intro_from_broker", "key_info_table", "floorplan", "broker_description", "middle_banner", "realtor_map", "realtor_info", "legal_disclosure", "seo_footer"],
        ],
    }

    flows = base_flows.get(story_profile, base_flows["balanced_human_story"])
    flow = list(flows[variant_index % len(flows)])

    # 사용 불가능한 블록 제거
    flow = [b for b in flow if b in blocks]

    # 필수 블록이 빠졌으면 후반에 보강
    for rb in REQUIRED_BLOCKS:
        if rb not in flow:
            flow.append(rb)

    # 중복 제거
    seen = set()
    final = []
    for block in flow:
        if block not in seen:
            final.append(block)
            seen.add(block)

    score = score_layout(final, signals, story_profile)

    return {
        "layout_id": f"{story_profile}_v{variant_index + 1:02d}",
        "story_profile": story_profile,
        "blocks": final,
        "score": score["score"],
        "score_detail": score,
    }


def score_layout(blocks, signals, story_profile):
    score = 100
    penalties = []
    bonuses = []

    # 필수 누락 방지
    for rb in REQUIRED_BLOCKS:
        if rb not in blocks:
            score -= 40
            penalties.append(f"required_missing:{rb}")

    # 너무 표가 앞에 오면 사람 느낌 감소
    if "key_info_table" in blocks and blocks.index("key_info_table") <= 1:
        score -= 8
        penalties.append("key_info_too_early")

    # 사람형 도입부가 앞에 있으면 가산
    if "human_intro_from_broker" in blocks and blocks.index("human_intro_from_broker") <= 2:
        score += 6
        bonuses.append("human_intro_early")

    # 중개사 설명이 너무 뒤면 감점
    if "broker_description" in blocks and blocks.index("broker_description") > 7:
        score -= 5
        penalties.append("broker_desc_too_late")

    # 사진 많을 때 photo_story가 앞쪽에 있으면 가산
    if signals["image_counts"]["total"] >= 8 and "photo_story" in blocks and blocks.index("photo_story") <= 4:
        score += 5
        bonuses.append("photo_story_early")

    # 학군/대단지형이면 커뮤니티 노트가 앞쪽에 있으면 가산
    if story_profile in ["family_school_story", "large_complex_living_story"]:
        if "community_or_school_note" in blocks and blocks.index("community_or_school_note") <= 4:
            score += 6
            bonuses.append("school_living_note_early")
        else:
            score -= 8
            penalties.append("school_living_note_late_or_missing")

    # 지도는 너무 앞보다 후반이 자연스러움
    if "realtor_map" in blocks and blocks.index("realtor_map") < 6:
        score -= 5
        penalties.append("map_too_early")

    # 법정표시는 후반이 자연스러움
    if "legal_disclosure" in blocks and blocks.index("legal_disclosure") < len(blocks) - 3:
        score -= 4
        penalties.append("legal_too_early")

    score = max(0, min(120, score))

    return {
        "score": score,
        "penalties": penalties,
        "bonuses": bonuses,
    }


def generate_layouts(signals, count):
    story_profile = story_profile_from_signal(signals["top_signal"])
    candidates = []
    for idx in range(max(count, 1) * 2):
        cand = compose_candidate(signals, story_profile, idx)
        if cand["blocks"] not in [x["blocks"] for x in candidates]:
            candidates.append(cand)

    candidates = sorted(candidates, key=lambda x: x["score"], reverse=True)
    return {
        "story_profile": story_profile,
        "candidates": candidates[:count],
    }


def print_report(signals, result):
    print("=" * 80)
    print("[STEP111-02 HBE LAYOUT COMPOSER]")
    print("draft_id:", signals.get("draft_id"))
    print("realtor_id:", signals.get("realtor_id"))
    print("article_no:", signals.get("article_no"))
    print("title:", signals.get("title"))
    print("top_signal:", signals.get("top_signal"))
    print("story_profile:", result.get("story_profile"))
    print("image_counts:", json.dumps(signals.get("image_counts"), ensure_ascii=False))
    print("broker_desc_len:", signals.get("broker_desc_len"))
    print("-" * 80)

    for idx, cand in enumerate(result["candidates"], start=1):
        print(f"[CANDIDATE {idx}] {cand['layout_id']} score={cand['score']}")
        if cand["score_detail"].get("bonuses"):
            print(" bonuses:", ", ".join(cand["score_detail"]["bonuses"]))
        if cand["score_detail"].get("penalties"):
            print(" penalties:", ", ".join(cand["score_detail"]["penalties"]))
        print(" blocks:")
        for i, block in enumerate(cand["blocks"], start=1):
            print(f"  {i:02d}. {block}")
        print("-" * 80)

    print("[NOTE]")
    print("- 현재 도구는 읽기 전용입니다. 운영 초안은 변경하지 않습니다.")
    print("- 다음 단계에서 선택된 layout_id를 실제 초안 생성에 적용하는 실험용 렌더러를 만들 예정입니다.")
    print("=" * 80)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--draft-id", type=int, default=None)
    parser.add_argument("--realtor-id", type=int, default=None)
    parser.add_argument("--article-no", default="")
    parser.add_argument("--count", type=int, default=3)
    parser.add_argument("--json", action="store_true")
    args = parser.parse_args()

    if not args.draft_id and not (args.realtor_id and args.article_no):
        raise RuntimeError("--draft-id 또는 --realtor-id + --article-no 필요")

    conn = get_conn()
    try:
        draft = fetch_draft(conn, draft_id=args.draft_id, realtor_id=args.realtor_id, article_no=args.article_no)
        if not draft:
            raise RuntimeError("draft not found")

        signals = analyze_signals(draft)
        result = generate_layouts(signals, args.count)

        payload = {"signals": signals, "layout_result": result}

        if args.json:
            print(json.dumps(payload, ensure_ascii=False, indent=2, default=str))
        else:
            print_report(signals, result)
    finally:
        conn.close()


if __name__ == "__main__":
    main()
