#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
classify_organize.py
=====================
TopEduPrep 학생용 학습페이지(워크북) 3,000개를 스캔해서
과목 / 학년 / 챕터를 자동 추출하고, study.topeduprep.uk용 폴더 구조로 정리합니다.

사용법
------
1. 아래 SOURCE_DIR 경로를 실제 3,000개 파일이 있는 폴더로 수정
2. 처음엔 반드시 DRY_RUN = True 로 먼저 실행해서 분류 결과를 확인 (파일 이동 없음)
3. 결과가 마음에 들면 DRY_RUN = False 로 바꿔서 실제로 폴더 정리
4. 실행: python classify_organize.py

설치 필요 패키지
----------------
없음 (표준 라이브러리만 사용 - BeautifulSoup 없이 정규식으로 파싱)
"""

import os
import re
import csv
import json
import shutil
import hashlib
from pathlib import Path
from datetime import datetime

# ============================================================
# 설정 (사용 전 반드시 확인/수정)
# ============================================================

SOURCE_DIR = r"C:\topeduprep\pages"          # ← 3,000개 파일이 있는 실제 폴더로 수정하세요
OUTPUT_DIR = r"C:\topeduprep\study_organized"  # ← 정리된 결과가 저장될 폴더
REPORT_CSV = os.path.join(OUTPUT_DIR, "_classification_report.csv")
MANIFEST_JSON = os.path.join(OUTPUT_DIR, "_manifest.json")  # 관리자 대시보드가 읽는 파일

DRY_RUN = True          # True면 실제 파일 복사/이동 없이 분류 리포트만 생성
COPY_NOT_MOVE = True    # True면 복사(원본 보존), False면 이동(원본 삭제) — 처음엔 복사 권장
LOW_CONFIDENCE_THRESHOLD = 2   # 이 점수 미만이면 "검토 필요" 폴더로 분류

# ============================================================
# 분류 기준 taxonomy
# 필요에 맞게 키워드를 자유롭게 추가/수정하세요.
# 각 키워드는 파일명 + title + h1 + meta keywords/description 전체에서 검색됩니다.
# ============================================================

CATEGORIES = {
    "math": {
        "label": "수학",
        "keywords": ["math", "수학", "algebra", "geometry", "calculus", "trigonometry",
                     "vieta", "공통수학", "대수", "기하", "미적분", "삼각함수"],
        "subjects": {
            "pre-algebra":   ["pre-algebra", "prealgebra", "프리알지브라"],
            "algebra1":      ["algebra1", "algebra 1", "algebra-1", "대수1", "대수 1"],
            "algebra2":      ["algebra2", "algebra 2", "algebra-2", "대수2", "대수 2"],
            "geometry":      ["geometry", "기하"],
            "precalculus":   ["pre-calculus", "precalculus", "pre calc", "미적분 전"],
            "trigonometry":  ["trigonometry", "trig", "삼각함수"],
            "common-math1":  ["공통수학1", "공통수학 1"],
            "common-math2":  ["공통수학2", "공통수학 2"],
            "ms-math1":      ["중1수학", "중1 수학", "중학교 1학년 수학"],
            "ms-math2":      ["중2수학", "중2 수학", "중학교 2학년 수학"],
            "ms-math3":      ["중3수학", "중3 수학", "중학교 3학년 수학"],
        }
    },
    "science": {
        "label": "과학",
        "keywords": ["science", "과학", "biology", "chemistry", "physics", "생물", "화학", "물리"],
        "subjects": {
            "biology":   ["biology", "생물"],
            "chemistry": ["chemistry", "화학"],
            "physics":   ["physics", "물리"],
        }
    },
    "english": {
        "label": "영어",
        "keywords": ["english", "영어", "vocabulary", "grammar", "reading", "writing", "어휘", "문법", "독해"],
        "subjects": {
            "vocabulary": ["vocabulary", "어휘", "vocab"],
            "grammar":    ["grammar", "문법"],
            "reading":    ["reading", "독해"],
            "writing":    ["writing", "작문"],
        }
    },
    "history": {
        "label": "역사",
        "keywords": ["history", "역사", "world history", "us history"],
        "subjects": {
            "world-history": ["world history", "세계사"],
            "us-history":    ["us history", "미국사"],
        }
    },
}

GRADE_KEYWORDS = {
    "ms1": ["중1", "middle school 1", "grade 7", "7학년"],
    "ms2": ["중2", "middle school 2", "grade 8", "8학년"],
    "ms3": ["중3", "middle school 3", "grade 9", "9학년"],
    "hs1": ["고1", "high school 1", "grade 10", "10학년"],
}

# 시험 트랙 (SAT/IB/AP) - 학원 비즈니스 특성상 같이 태깅
EXAM_KEYWORDS = {
    "sat": ["sat"],
    "ib":  ["ib", "international baccalaureate"],
    "ap":  ["ap ", "advanced placement"],
}

UNCATEGORIZED = "_needs_review"

# ============================================================
# HTML 파싱 (정규식 기반 — 외부 라이브러리 불필요)
# ============================================================

def extract_html_metadata(text: str) -> dict:
    """HTML 텍스트에서 title, h1, meta keywords/description을 추출"""
    def first_match(pattern, s, flags=re.IGNORECASE | re.DOTALL):
        m = re.search(pattern, s, flags)
        if m:
            return re.sub(r"<[^>]+>", " ", m.group(1)).strip()
        return ""

    title = first_match(r"<title[^>]*>(.*?)</title>", text)
    h1 = first_match(r"<h1[^>]*>(.*?)</h1>", text)

    meta_keywords = ""
    m = re.search(r'<meta\s+name=["\']keywords["\']\s+content=["\'](.*?)["\']', text, re.IGNORECASE)
    if m:
        meta_keywords = m.group(1)

    meta_desc = ""
    m = re.search(r'<meta\s+name=["\']description["\']\s+content=["\'](.*?)["\']', text, re.IGNORECASE)
    if m:
        meta_desc = m.group(1)

    return {
        "title": title,
        "h1": h1,
        "meta_keywords": meta_keywords,
        "meta_description": meta_desc,
    }


def slugify(text: str, max_len: int = 60) -> str:
    """파일/폴더명으로 쓸 수 있게 안전한 슬러그로 변환"""
    text = text.strip().lower()
    text = re.sub(r"[^\w\s가-힣-]", "", text)
    text = re.sub(r"\s+", "-", text)
    return text[:max_len].strip("-") or "untitled"


# ============================================================
# 분류 로직
# ============================================================

def classify_file(filepath: Path) -> dict:
    try:
        raw = filepath.read_text(encoding="utf-8", errors="ignore")
    except Exception as e:
        return {
            "filename": filepath.name,
            "error": str(e),
            "category": UNCATEGORIZED,
            "subject": "",
            "grade": "",
            "exam": "",
            "confidence": 0,
            "topic_slug": slugify(filepath.stem),
        }

    meta = extract_html_metadata(raw)
    haystack = " ".join([
        filepath.stem.replace("-", " ").replace("_", " "),
        meta["title"], meta["h1"], meta["meta_keywords"], meta["meta_description"]
    ]).lower()

    # 1) 대분류(category) 점수 계산
    best_cat, best_cat_score = UNCATEGORIZED, 0
    for cat_key, cat_data in CATEGORIES.items():
        score = sum(1 for kw in cat_data["keywords"] if kw.lower() in haystack)
        if score > best_cat_score:
            best_cat, best_cat_score = cat_key, score

    # 2) 세부 과목(subject) 점수 계산 (대분류가 정해졌을 때만)
    best_subject, best_subject_score = "", 0
    if best_cat != UNCATEGORIZED:
        for subj_key, subj_kws in CATEGORIES[best_cat]["subjects"].items():
            score = sum(1 for kw in subj_kws if kw.lower() in haystack)
            if score > best_subject_score:
                best_subject, best_subject_score = subj_key, score

    # 3) 학년(grade) 추출
    best_grade = ""
    for grade_key, kws in GRADE_KEYWORDS.items():
        if any(kw.lower() in haystack for kw in kws):
            best_grade = grade_key
            break

    # 4) 시험 트랙(exam) 추출
    best_exam = ""
    for exam_key, kws in EXAM_KEYWORDS.items():
        if any(kw.lower() in haystack for kw in kws):
            best_exam = exam_key
            break

    confidence = best_cat_score + best_subject_score

    # 5) 챕터/토픽 슬러그 — h1 우선, 없으면 title, 없으면 파일명
    topic_source = meta["h1"] or meta["title"] or filepath.stem
    topic_slug = slugify(topic_source)

    if confidence < LOW_CONFIDENCE_THRESHOLD:
        category_final = UNCATEGORIZED
    else:
        category_final = best_cat

    return {
        "filename": filepath.name,
        "title": meta["title"],
        "h1": meta["h1"],
        "category": category_final,
        "subject": best_subject,
        "grade": best_grade,
        "exam": best_exam,
        "confidence": confidence,
        "topic_slug": topic_slug,
        "error": "",
    }


def build_target_path(result: dict) -> Path:
    """분류 결과를 바탕으로 study_organized/ 안의 목표 경로 생성"""
    if result["category"] == UNCATEGORIZED or not result["category"]:
        parts = [UNCATEGORIZED]
    else:
        parts = [result["category"]]
        if result["subject"]:
            parts.append(result["subject"])
        if result["grade"]:
            parts.append(result["grade"])

    folder = Path(OUTPUT_DIR, *parts)
    filename = f"{result['topic_slug']}.html"
    return folder / filename


def dedupe_path(path: Path) -> Path:
    """동일 경로에 파일이 이미 있으면 -2, -3 ... 붙여서 중복 방지"""
    if not path.exists():
        return path
    stem, suffix = path.stem, path.suffix
    i = 2
    while True:
        candidate = path.with_name(f"{stem}-{i}{suffix}")
        if not candidate.exists():
            return candidate
        i += 1


# ============================================================
# 메인 실행
# ============================================================

def main():
    source = Path(SOURCE_DIR)
    if not source.exists():
        print(f"[오류] SOURCE_DIR 경로가 존재하지 않습니다: {SOURCE_DIR}")
        print("스크립트 상단의 SOURCE_DIR 값을 실제 폴더 경로로 수정해주세요.")
        return

    html_files = sorted(list(source.glob("*.html")) + list(source.glob("*.htm")))
    if not html_files:
        print(f"[경고] {SOURCE_DIR} 안에서 .html 파일을 찾지 못했습니다.")
        return

    print(f"총 {len(html_files)}개 파일 발견. 분류를 시작합니다...")
    print(f"DRY_RUN = {DRY_RUN}  (True면 실제 파일 이동/복사 없음)\n")

    results = []
    manifest_entries = []
    category_counts = {}

    for idx, fp in enumerate(html_files, 1):
        result = classify_file(fp)
        stat = fp.stat()

        target_path = build_target_path(result)

        if not DRY_RUN:
            target_path.parent.mkdir(parents=True, exist_ok=True)
            final_target = dedupe_path(target_path)
            if COPY_NOT_MOVE:
                shutil.copy2(fp, final_target)
            else:
                shutil.move(str(fp), str(final_target))
        else:
            final_target = target_path  # 미리보기용 (실제 생성 안 됨)

        rel_target = os.path.relpath(final_target, OUTPUT_DIR) if not DRY_RUN else os.path.relpath(target_path, OUTPUT_DIR)

        category_counts[result["category"]] = category_counts.get(result["category"], 0) + 1

        row = {
            "original_filename": fp.name,
            "original_path": str(fp),
            "title": result["title"],
            "category": result["category"],
            "subject": result["subject"],
            "grade": result["grade"],
            "exam": result["exam"],
            "confidence": result["confidence"],
            "new_relative_path": rel_target.replace("\\", "/"),
            "file_size_kb": round(stat.st_size / 1024, 1),
            "last_modified": datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M"),
            "error": result["error"],
        }
        results.append(row)

        # 관리자 대시보드용 manifest 항목 (id는 파일 해시로 안정적인 고유값 생성)
        file_id = hashlib.md5(fp.name.encode("utf-8")).hexdigest()[:10]
        manifest_entries.append({
            "id": file_id,
            "name": fp.name,
            "title": result["title"] or fp.stem,
            "category": result["category"],
            "subject": result["subject"],
            "grade": result["grade"],
            "exam": result["exam"],
            "confidence": result["confidence"],
            "path": rel_target.replace("\\", "/"),
            "size_kb": row["file_size_kb"],
            "modified": row["last_modified"],
        })

        if idx % 250 == 0:
            print(f"  ...{idx}/{len(html_files)} 처리됨")

    # CSV 리포트 저장
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    with open(REPORT_CSV, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=list(results[0].keys()))
        writer.writeheader()
        writer.writerows(results)

    # manifest.json 저장 (관리자 대시보드가 읽는 파일)
    with open(MANIFEST_JSON, "w", encoding="utf-8") as f:
        json.dump({
            "generated_at": datetime.now().isoformat(),
            "total_files": len(manifest_entries),
            "files": manifest_entries,
        }, f, ensure_ascii=False, indent=2)

    print("\n" + "=" * 50)
    print("분류 완료!")
    print("=" * 50)
    for cat, count in sorted(category_counts.items(), key=lambda x: -x[1]):
        label = CATEGORIES.get(cat, {}).get("label", cat)
        print(f"  {label:10s} ({cat:12s}): {count}개")
    print(f"\n리포트 CSV: {REPORT_CSV}")
    print(f"매니페스트 JSON: {MANIFEST_JSON}")
    if DRY_RUN:
        print("\n⚠ DRY_RUN=True 상태였으므로 실제 파일은 이동/복사되지 않았습니다.")
        print("  CSV를 열어서 분류 결과를 확인한 후, 문제 없으면 DRY_RUN = False 로 바꿔서 다시 실행하세요.")


if __name__ == "__main__":
    main()
