#!/usr/bin/env python3
"""gen_*.py / rerender_*.py 하드코딩 마이그레이션 스크립트.

실행: python3 _migrate_hardcodes.py

수행 작업:
  A. 경로 하드코딩 → WORKSPACE_ROOT / "..." 참조
  B. font-size: Npx → config 상수 참조
  C. line-height: N → config 상수 참조
  D. FONT_DIR 정의 제거 (gen_config 에서 import)
  E. @font-face 하드코딩 경로 → FONT_DIR 참조
  F. gen_config import 추가
"""

from __future__ import annotations

import re
import shutil
from pathlib import Path

# ── 대상 디렉토리 ──────────────────────────────────────────────────────────────
AI_DIR = Path(__file__).resolve().parent
BACKUP_DIR = AI_DIR / "_backup"

# ── DQ 매핑 (px 값 → 상수명 / config 참조) ────────────────────────────────────
FONT_SIZE_MAP: dict[int, tuple[str, str]] = {
    96: ("_METRIC_PX", "CORE_METRIC_MIN_PX"),
    84: ("_HEADLINE_PX", "HEADLINE_MIN_PX"),
    64: ("_SUBHEAD_PX", "SUBHEAD_MIN_PX"),
    40: ("_CTA_PX", "CTA_MIN_PX"),
}

LINE_HEIGHT_MAP: dict[str, tuple[str, str]] = {
    "1.3": ("_LH_RATIO", "HEAD_SUB_RATIO"),
}


def _lh_var_name(val: str) -> str:
    """line-height 값에 대한 변수명 생성."""
    if val in LINE_HEIGHT_MAP:
        return LINE_HEIGHT_MAP[val][0]
    safe = val.replace(".", "_")
    return f"_LH_{safe}"


def _fs_var_name(px: int) -> str:
    """font-size px 값에 대한 변수명 생성."""
    if px in FONT_SIZE_MAP:
        return FONT_SIZE_MAP[px][0]
    return f"_SIZE_{px}PX"


def _fs_var_value(px: int) -> str:
    """font-size px 값에 대한 우변 값 생성."""
    if px in FONT_SIZE_MAP:
        return FONT_SIZE_MAP[px][1]
    return str(px)


def _lh_var_value(val: str) -> str:
    """line-height 값에 대한 우변 값 생성."""
    if val in LINE_HEIGHT_MAP:
        return LINE_HEIGHT_MAP[val][1]
    return val


def collect_font_sizes(source: str) -> list[int]:
    """소스에서 font-size: Npx 패턴의 고유 px 값 수집."""
    vals = set(int(m) for m in re.findall(r"font-size:\s*(\d+)px", source))
    return sorted(vals)


def collect_line_heights(source: str) -> list[str]:
    """소스에서 line-height: N (단위 없는) 패턴의 고유 값 수집.

    px 단위가 붙은 것은 별도 처리이므로 여기서는 제외.
    """
    all_vals = re.findall(r"line-height:\s*([0-9]+(?:\.[0-9]+)?)(?:\s*;|\s*}|\s*$)", source)
    # px 값(정수이면서 >= 10)은 line-height-as-ratio가 아니라 px 기반이므로 제외
    ratio_vals = []
    seen = set()
    for v in all_vals:
        # 10 이상의 정수는 px 기반으로 간주 — 이미 font-size와 동일하게 처리하거나 스킵
        try:
            fv = float(v)
        except ValueError:
            continue
        if fv >= 10:
            # px-value line-heights: 나중에 font-size와 동일한 방식으로 처리
            continue
        if v not in seen:
            seen.add(v)
            ratio_vals.append(v)
    return ratio_vals


def collect_line_heights_px(source: str) -> list[int]:
    """line-height: Npx 패턴의 고유 px 값 수집."""
    vals = set(int(m) for m in re.findall(r"line-height:\s*(\d+)px", source))
    return sorted(vals)


def collect_line_heights_no_unit(source: str) -> list[str]:
    """line-height: N (단위없는, >= 10 정수) 패턴의 고유 값 수집."""
    all_vals = re.findall(r"line-height:\s*([0-9]+(?:\.[0-9]+)?)(?:\s*[;}\n\r])", source)
    large_int_vals = []
    seen: set[str] = set()
    for v in all_vals:
        try:
            fv = float(v)
        except ValueError:
            continue
        if fv >= 10 and v not in seen:
            seen.add(v)
            large_int_vals.append(v)
    return large_int_vals


# ─────────────────────────────────────────────────────────────────────────────
# is_fstring_file: 파일이 f-string 기반 HTML 생성인지 판단
# ─────────────────────────────────────────────────────────────────────────────

def is_fstring_html_file(source: str) -> bool:
    """return f\"\"\", = f\"\"\" 패턴을 통해 f-string HTML 파일인지 판단."""
    return bool(re.search(r'(return|=)\s*f"""', source))


def has_plain_html_vars(source: str) -> bool:
    """HTML_XXX = \"\"\"...\"\"\" 패턴 (non-f-string) 이 있는지 판단."""
    return bool(re.search(r'\bHTML_\w+\s*=\s*"""', source))


# ─────────────────────────────────────────────────────────────────────────────
# 경로 마이그레이션 헬퍼
# ─────────────────────────────────────────────────────────────────────────────

_WS = "/home/jay/workspace"
_FONTS_HARD = "/home/jay/.local/share/fonts/Pretendard"

def migrate_paths(source: str) -> tuple[str, int]:
    """하드코딩된 /home/jay/workspace 경로를 WORKSPACE_ROOT 참조로 치환.
    반환: (수정된 소스, 치환 수)
    """
    count = 0

    # 1. Path("/home/jay/workspace/...") → WORKSPACE_ROOT / "..."
    def repl_path_ctor(m: re.Match) -> str:
        inner = m.group(1)
        rel = inner[len(_WS):].lstrip("/")
        if rel:
            return f'WORKSPACE_ROOT / "{rel}"'
        else:
            return "WORKSPACE_ROOT"

    new_source, n = re.subn(
        r'Path\("(/home/jay/workspace[^"]*?)"\)',
        repl_path_ctor,
        source
    )
    count += n
    source = new_source

    # 2. "/home/jay/workspace/..." 순수 문자열 (Path() 생성자 외부)
    def repl_str_ws(m: re.Match) -> str:
        inner = m.group(1)
        rel = inner[len(_WS):].lstrip("/")
        if rel:
            return f'str(WORKSPACE_ROOT / "{rel}")'
        else:
            return "str(WORKSPACE_ROOT)"

    new_source, n = re.subn(
        r'"(/home/jay/workspace[^"]*?)"',
        repl_str_ws,
        source
    )
    count += n
    source = new_source

    return source, count


def migrate_font_dir_def(source: str) -> tuple[str, int]:
    """FONT_DIR = Path.home() / ... 정의를 제거 (gen_config에서 import).
    반환: (수정된 소스, 제거 수)
    """
    # 여러 패턴 처리:
    # FONT_DIR = Path.home() / ".local/share/fonts/Pretendard"
    # FONT_DIR = Path.home() / ".local/share/fonts" / "Pretendard"
    # 여러 줄 포함 가능
    patterns = [
        r'\n?FONT_DIR\s*=\s*Path\.home\(\)\s*/\s*"\.local/share/fonts/Pretendard"\s*\n',
        r'\n?FONT_DIR\s*=\s*Path\.home\(\)\s*/\s*"\.local/share/fonts"\s*/\s*"Pretendard"\s*\n',
    ]
    count = 0
    for pat in patterns:
        new_source, n = re.subn(pat, "\n", source)
        count += n
        source = new_source
    return source, count


def migrate_inline_font_paths(source: str) -> tuple[str, int]:
    """@font-face 내 하드코딩된 파일 경로를 FONT_DIR 참조로 치환.

    f-string 내부: file:///home/jay/.local/share/fonts/Pretendard/Pretendard-X.otf
    → file://{FONT_DIR}/Pretendard-X.otf

    일반 string 내부: file:///home/jay/.local/share/fonts/Pretendard/Pretendard-X.otf
    → 일반 string이라면 f-string으로 변환이 필요하지만, 이 스크립트는
      상수 블록으로 치환하는 방식을 사용.
    """
    count = 0

    # f-string 내부 혹은 직접 참조: file:///home/jay/.local/share/fonts/Pretendard/
    # → file://{FONT_DIR}/
    new_source, n = re.subn(
        r"file:///home/jay/\.local/share/fonts/Pretendard/",
        "file://{FONT_DIR}/",
        source
    )
    count += n
    source = new_source

    # Path("/home/jay/.local/share/fonts/Pretendard") 형태
    new_source, n = re.subn(
        r'Path\("/home/jay/\.local/share/fonts/Pretendard"\)',
        "FONT_DIR",
        source
    )
    count += n
    source = new_source

    # "/home/jay/.local/share/fonts/Pretendard" 순수 문자열
    new_source, n = re.subn(
        r'"/home/jay/\.local/share/fonts/Pretendard"',
        "str(FONT_DIR)",
        source
    )
    count += n
    source = new_source

    return source, count


def migrate_font_face_block_in_plain_string(source: str) -> tuple[str, int]:
    """HTML_XXX = \"\"\"...\"\"\" 블록 내 @font-face 하드코딩 처리.

    일반 문자열 내부는 f-string이 아니므로 {FONT_DIR} 치환이 불가.
    대신, file:///home/jay/.local/share/fonts/Pretendard/ 를
    {_FONT_DIR_PLACEHOLDER_} 으로 바꾸고 나중에 _apply_font_config 에 넣는
    방식은 복잡도를 높이므로, 더 실용적인 방법을 사용:

    HTML_XXX = \"\"\"...\"\"\" 를 HTML_XXX = f\"\"\"...\"\"\" 로 변환하고
    내부의 {{ }} 이스케이프를 적용한다.
    """
    count = 0

    # 일반 HTML_XXX = """...""" 블록에 @font-face 하드코딩이 있으면 f-string으로 변환
    def convert_plain_html_to_fstring(m: re.Match) -> str:
        varname = m.group(1)
        content = m.group(2)
        if "file:///home/jay/.local/share/fonts/Pretendard" not in content:
            return m.group(0)  # 변경 없음
        # 기존 { } 를 {{ }} 로 이스케이프 (이미 {{ }} 된 것 제외)
        # 단순 접근: 이미 {{ 이 있으면 이미 f-string 스타일이므로 패스
        if "{{" in content:
            # 이미 이스케이프된 상태이면 그냥 f-string prefix만 추가
            new_content = content
        else:
            # { } 를 {{ }} 로 이스케이프
            new_content = content.replace("{", "{{").replace("}", "}}")
        # 폰트 경로 치환 (이제 f-string이므로 {FONT_DIR} 사용 가능)
        new_content = new_content.replace(
            "file:///home/jay/.local/share/fonts/Pretendard/",
            "file://{FONT_DIR}/"
        )
        return f'{varname} = f"""{new_content}"""'

    new_source, n = re.subn(
        r'(HTML_\w+)\s*=\s*"""(.*?)"""',
        convert_plain_html_to_fstring,
        source,
        flags=re.DOTALL
    )
    count += n
    source = new_source

    return source, count


# ─────────────────────────────────────────────────────────────────────────────
# font-size / line-height 마이그레이션
# ─────────────────────────────────────────────────────────────────────────────

def build_size_constants(font_sizes: list[int], lh_ratios: list[str]) -> str:
    """파일 상단에 추가할 폰트 사이즈 / line-height 상수 정의 블록."""
    lines = []
    for px in font_sizes:
        var = _fs_var_name(px)
        val = _fs_var_value(px)
        lines.append(f"{var} = {val}")
    for lh in lh_ratios:
        var = _lh_var_name(lh)
        val = _lh_var_value(lh)
        lines.append(f"{var} = {val}")
    return "\n".join(lines) + "\n" if lines else ""


def replace_font_sizes_in_source(source: str, font_sizes: list[int]) -> tuple[str, int]:
    """소스 내 font-size: Npx 를 {_VAR_PX}px 또는 변수명 참조로 치환."""
    count = 0
    for px in font_sizes:
        var = _fs_var_name(px)
        pattern = f"font-size: {px}px"
        replacement = f"font-size: {{{var}}}px"
        n = source.count(pattern)
        if n:
            source = source.replace(pattern, replacement)
            count += n
    return source, count


def replace_line_heights_in_source(source: str, lh_ratios: list[str]) -> tuple[str, int]:
    """소스 내 line-height: N 를 {_LH_VAR} 또는 변수명으로 치환."""
    count = 0
    for lh in lh_ratios:
        var = _lh_var_name(lh)
        # line-height: 1.3; 또는 line-height: 1.3\n 패턴
        pattern = f"line-height: {lh}"
        replacement = f"line-height: {{{var}}}"
        n = source.count(pattern)
        if n:
            source = source.replace(pattern, replacement)
            count += n
    return source, count


# ─────────────────────────────────────────────────────────────────────────────
# apply_font_config 헬퍼 (plain string HTML 파일용) — 현재는 f-string 변환으로 대체
# 이 함수는 font-size 치환을 위해 사용됨
# ─────────────────────────────────────────────────────────────────────────────

def build_apply_font_config_func(font_sizes: list[int], lh_ratios: list[str]) -> str:
    """_apply_font_config() 헬퍼 함수 소스 생성."""
    subs_lines = []
    for px in font_sizes:
        var = _fs_var_name(px)
        subs_lines.append(f'        "font-size: {px}px": f"font-size: {{{var}}}px",')
    for lh in lh_ratios:
        var = _lh_var_name(lh)
        subs_lines.append(f'        "line-height: {lh}": f"line-height: {{{var}}}",')
    subs_str = "\n".join(subs_lines)
    return f'''

def _apply_font_config(html: str) -> str:
    """폰트 사이즈 하드코딩을 config 값으로 치환."""
    subs = {{
{subs_str}
    }}
    for old, new in subs.items():
        html = html.replace(old, new)
    return html
'''


# ─────────────────────────────────────────────────────────────────────────────
# import 블록 결정
# ─────────────────────────────────────────────────────────────────────────────

def build_import_line(
    uses_workspace: bool,
    uses_font_dir: bool,
    font_sizes: list[int],
    lh_ratios: list[str],
) -> str:
    """실제로 필요한 것만 import."""
    imports = []
    if uses_workspace:
        imports.append("WORKSPACE_ROOT")
    if uses_font_dir:
        imports.append("FONT_DIR")

    needed_consts = set()
    for px in font_sizes:
        if px in FONT_SIZE_MAP:
            needed_consts.add(FONT_SIZE_MAP[px][1])
    for lh in lh_ratios:
        if lh in LINE_HEIGHT_MAP:
            needed_consts.add(LINE_HEIGHT_MAP[lh][1])

    imports.extend(sorted(needed_consts))

    if not imports:
        return ""

    imports_str = ", ".join(imports)
    return f"from gen_config import {imports_str}\n"


# ─────────────────────────────────────────────────────────────────────────────
# 메인 파일 처리 함수
# ─────────────────────────────────────────────────────────────────────────────

def find_import_insertion_point(source: str) -> int:
    """import 구문 삽입 위치 (마지막 import/from 라인 다음) 반환 (문자 인덱스)."""
    lines = source.splitlines(keepends=True)
    last_import_idx = -1
    for i, line in enumerate(lines):
        stripped = line.strip()
        if stripped.startswith("import ") or stripped.startswith("from "):
            last_import_idx = i
    if last_import_idx == -1:
        # import 없으면 shebang/docstring 이후
        for i, line in enumerate(lines):
            stripped = line.strip()
            if stripped and not stripped.startswith("#") and not stripped.startswith('"""') and not stripped.startswith("'''"):
                return sum(len(l) for l in lines[:i])
        return 0
    return sum(len(l) for l in lines[: last_import_idx + 1])


def find_constants_insertion_point(source: str) -> int:
    """상수 정의 삽입 위치 (import 블록 이후) 반환 (문자 인덱스)."""
    return find_import_insertion_point(source)


def process_file(fpath: Path) -> dict:
    """파일을 읽고 마이그레이션을 적용하여 덮어쓴다."""
    source = fpath.read_text(encoding="utf-8")
    original = source
    changes: list[str] = []

    # ── 이미 gen_config import가 있으면 스킵 ──
    if "from gen_config import" in source:
        return {"file": fpath.name, "skipped": True, "reason": "already migrated"}

    # ── A. 경로 마이그레이션 ──
    source, n_paths = migrate_paths(source)
    if n_paths:
        changes.append(f"경로 치환 {n_paths}건")

    # ── B. FONT_DIR 정의 제거 ──
    source, n_fdir = migrate_font_dir_def(source)
    if n_fdir:
        changes.append(f"FONT_DIR 정의 제거 {n_fdir}건")

    # ── C. 인라인 @font-face 경로 치환 ──
    #    일반 HTML_XXX = """...""" 블록 처리 먼저 (f-string 변환 포함)
    source, n_plain = migrate_font_face_block_in_plain_string(source)
    if n_plain:
        changes.append(f"plain HTML→fstring 변환 {n_plain}건")

    #    나머지 인라인 폰트 경로 (f-string 내부 또는 독립 변수)
    source, n_fp = migrate_inline_font_paths(source)
    if n_fp:
        changes.append(f"폰트 경로 치환 {n_fp}건")

    # ── D. font-size / line-height 수집 & 치환 ──
    font_sizes = collect_font_sizes(source)
    lh_ratios = collect_line_heights(source)

    # font-size 치환 (f-string 방식으로 통일 — source 내 이미 {{ }} 이스케이프 된 것 고려)
    source, n_fs = replace_font_sizes_in_source(source, font_sizes)
    if n_fs:
        changes.append(f"font-size 치환 {n_fs}건")

    source, n_lh = replace_line_heights_in_source(source, lh_ratios)
    if n_lh:
        changes.append(f"line-height 치환 {n_lh}건")

    # ── E. 필요한 import 결정 ──
    uses_workspace = "WORKSPACE_ROOT" in source
    uses_font_dir = "FONT_DIR" in source
    import_line = build_import_line(uses_workspace, uses_font_dir, font_sizes, lh_ratios)

    if not import_line and not changes:
        return {"file": fpath.name, "skipped": True, "reason": "no changes needed"}

    # ── F. 상수 블록 생성 ──
    const_block = build_size_constants(font_sizes, lh_ratios)

    # ── G. 소스에 import 및 상수 삽입 ──
    if import_line or const_block:
        ins_point = find_import_insertion_point(source)
        insert_text = ""
        if import_line:
            insert_text += import_line
        if const_block:
            insert_text += const_block
        source = source[:ins_point] + insert_text + source[ins_point:]
        if import_line:
            changes.append("gen_config import 추가")
        if const_block:
            changes.append("상수 정의 추가")

    if source == original:
        return {"file": fpath.name, "skipped": True, "reason": "no effective changes"}

    # ── 백업 ──
    BACKUP_DIR.mkdir(exist_ok=True)
    backup_path = BACKUP_DIR / fpath.name
    shutil.copy2(fpath, backup_path)

    # ── 저장 ──
    fpath.write_text(source, encoding="utf-8")

    return {
        "file": fpath.name,
        "skipped": False,
        "changes": changes,
        "backup": str(backup_path),
    }


# ─────────────────────────────────────────────────────────────────────────────
# 진입점
# ─────────────────────────────────────────────────────────────────────────────

def main() -> None:
    targets = sorted(
        list(AI_DIR.glob("gen_*.py")) + list(AI_DIR.glob("rerender_*.py"))
    )
    # 이 스크립트 자신 및 gen_config 제외
    exclude = {AI_DIR / "_migrate_hardcodes.py", AI_DIR / "gen_config.py"}
    targets = [f for f in targets if f not in exclude]

    print(f"대상 파일 {len(targets)}개 처리 시작...\n")

    processed = 0
    skipped = 0

    for fpath in targets:
        result = process_file(fpath)
        if result.get("skipped"):
            reason = result.get("reason", "")
            print(f"  [SKIP] {result['file']} — {reason}")
            skipped += 1
        else:
            changes_str = ", ".join(result.get("changes", []))
            print(f"  [OK]   {result['file']} — {changes_str}")
            processed += 1

    print(f"\n완료: {processed}개 수정, {skipped}개 스킵, 백업: {BACKUP_DIR}")


if __name__ == "__main__":
    main()
