from __future__ import annotations

import json
from pathlib import Path

import numpy as np
import pandas as pd


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "outputs" / "may_resting_youth_classification"
OUT.mkdir(parents=True, exist_ok=True)

SOURCE_DIRS = [
    Path("/Users/minsung/Downloads/5월_청년층부가조사_20260512_13917_데이터"),
    Path("/Users/minsung/Downloads/5월_청년층부가조사_20260507_54935_데이터"),
]

USECOLS = [
    "조사연월",
    "성별코드",
    "교육정도_학력구분코드",
    "교육정도_계열코드",
    "교육정도_수학구분코드",
    "졸업연도",
    "만연령",
    "가중값",
    "경제활동상태코드",
    "구직사항_지난4주내구직여부",
    "기타활동사항_취업희망여부",
    "기타활동사항_취업가능성유무",
    "기타활동사항_4주내비구직사유코드",
    "기타활동사항_지난1년내구직활동유무",
    "기타활동사항_지난주주요활동상태코드",
    "이전직장사항_전직유무",
    "이전직장사항_퇴직기간1년미만여부",
    "이전직장사항_1년미만퇴직연월",
    "이전직장사항_이직사유코드",
    "이전직장_11차산업대분류코드",
    "이전직장_8차직업대분류코드",
    "이전직장_종사상지위코드",
    "가장최근학교_졸업및중퇴연월",
    "직업교육및직장체험_직업교육수혜구분코드",
    "직업교육및직장체험_취업관련시험준비여부",
    "직업교육및직장체험_취업관련시험준비분야코드",
    "직업교육및직장체험_재학휴학중직장체험여부",
    "직업교육및직장체험_직장체험주요형태코드",
    "직업교육및직장체험_재학휴학중직장체험기간코드",
    "졸업및중퇴후취업횟수코드",
    "첫직장_취업당시고용형태코드",
    "첫직장_임금근로자_전일제시간제여부",
    "첫직장_월평균급여액",
    "첫직장취업연월",
    "첫직장이직연월",
    "첫직장퇴직사유코드",
    "첫직장업종코드",
    "첫직장_근로형태코드",
    "최근직장_취업경로코드",
    "최근직장_전공관련성코드",
    "미취업_퇴직후미취업기간구분코드",
    "미취업_주요활동상태코드",
]

SEX_LABELS = {"1": "남성", "2": "여성"}
AGE_GROUP_LABELS = {
    "15-19": "15-19세",
    "20-24": "20-24세",
    "25-29": "25-29세",
}
EDU_LABELS = {
    "1": "초졸 이하",
    "2": "중졸",
    "3": "고등학교",
    "4": "전문대학",
    "5": "대학교",
    "6": "대학원",
}
FIELD_LABELS = {
    "1": "인문계열",
    "2": "사회계열",
    "3": "교육계열",
    "4": "공학계열",
    "5": "자연계열",
    "6": "의약계열",
    "7": "예체능계열",
}

# Labor-force May youth supplement coding observed in the microdata and
# cross-checked against the existing manuscript tables.
SCHOOL_STATUS_LABELS = {
    "1": "졸업",
    "2": "재학",
    "3": "중퇴",
    "4": "휴학",
    "5": "수료",
}
POST_SCHOOL_JOB_COUNT_LABELS = {
    "0": "재학·휴학 등 비해당",
    "1": "졸업·중퇴 후 취업경험 없음",
    "2": "1회",
    "3": "2회",
    "4": "3회",
    "5": "4회 이상",
}
UNEMP_DURATION_LABELS = {
    "0": "비해당",
    "1": "6개월 미만",
    "2": "6개월-1년 미만",
    "3": "1-2년 미만",
    "4": "2-3년 미만",
    "5": "3년 이상",
}
UNEMP_ACTIVITY_LABELS = {
    "0": "비해당",
    "1": "미취업 활동코드 1",
    "2": "미취업 활동코드 2",
    "3": "그냥 시간 보냄",
    "4": "미취업 활동코드 4",
    "5": "미취업 활동코드 5",
    "6": "미취업 활동코드 6",
    "7": "미취업 활동코드 7",
    "8": "미취업 활동코드 8",
}
OCC_LABELS = {
    "1": "관리자",
    "2": "전문가 및 관련 종사자",
    "3": "사무 종사자",
    "4": "서비스 종사자",
    "5": "판매 종사자",
    "6": "농림어업 숙련 종사자",
    "7": "기능원 및 관련 기능 종사자",
    "8": "장치·기계 조작 및 조립 종사자",
    "9": "단순노무 종사자",
}
PREVIOUS_JOB_STATUS_LABELS = {
    "0": "비해당",
    "1": "상용근로자",
    "2": "임시근로자",
    "3": "일용근로자",
    "4": "고용원 있는 자영업자",
    "5": "고용원 없는 자영업자",
    "6": "무급가족종사자",
}
QUIT_REASON_LABELS = {
    "0": "비해당/무응답",
    "1": "개인·가족 사유",
    "6": "근로여건 불만족",
    "9": "임시·계절 일자리 종료",
}

AI_OCC_SCORE = {
    "1": 42,
    "2": 55,
    "3": 82,
    "4": 38,
    "5": 70,
    "6": 25,
    "7": 45,
    "8": 58,
    "9": 34,
}

PRIMARY_ORDER = [
    "A. 재학·휴학 유예형",
    "B. 학교이탈 후 첫 일자리 미진입형",
    "C. 첫 일자리 1년 이하 단기이탈형",
    "D. 최근 1년 내 직장 이탈형",
    "E. 장기 비활동·저활동형",
    "F. 경력 있음·재진입 보류형",
    "G. 기타·정보부족형",
]


def normalize_code(value: object) -> str:
    if pd.isna(value):
        return ""
    text = str(value).strip()
    if not text:
        return ""
    if text.endswith(".0"):
        text = text[:-2]
    if text.isdigit():
        stripped = text.lstrip("0")
        return stripped if stripped else "0"
    return text


def normalize_month(value: object) -> str:
    if pd.isna(value):
        return ""
    text = "".join(ch for ch in str(value).strip() if ch.isdigit())
    if not text or set(text) == {"0"}:
        return ""
    if len(text) == 5:
        text = "0" + text
    return text if len(text) == 6 else ""


def month_index(value: object) -> float:
    text = normalize_month(value)
    if not text:
        return np.nan
    year = int(text[:4])
    month = int(text[4:6])
    if year < 1900 or year > 2035 or month < 1 or month > 12:
        return np.nan
    return year * 12 + month


def read_sources() -> pd.DataFrame:
    frames = []
    for directory in SOURCE_DIRS:
        for path in sorted(directory.glob("*.csv")):
            df = pd.read_csv(path, encoding="cp949", usecols=lambda c: c in USECOLS, dtype=str)
            df["source_file"] = path.name
            frames.append(df)
    if not frames:
        raise FileNotFoundError("No source CSV files found.")

    data = pd.concat(frames, ignore_index=True)
    for col in USECOLS:
        if col in data.columns and col not in {
            "조사연월",
            "가중값",
            "가장최근학교_졸업및중퇴연월",
            "첫직장취업연월",
            "첫직장이직연월",
            "이전직장사항_1년미만퇴직연월",
        }:
            data[col] = data[col].map(normalize_code)

    data["year"] = data["조사연월"].astype(str).str[:4].astype(int)
    data["age"] = pd.to_numeric(data["만연령"], errors="coerce")
    data["weight_persons"] = pd.to_numeric(data["가중값"], errors="coerce") / 1000.0
    data["weight_thousand"] = data["weight_persons"] / 1000.0
    data["age_group"] = pd.cut(
        data["age"],
        bins=[14, 19, 24, 29],
        labels=["15-19", "20-24", "25-29"],
    ).astype("string")
    return data


def add_features(data: pd.DataFrame) -> pd.DataFrame:
    df = data.copy()
    df["is_youth"] = df["age"].between(15, 29)
    df["is_inactive"] = df["경제활동상태코드"].eq("3")
    df["is_resting"] = df["기타활동사항_지난주주요활동상태코드"].eq("11")
    df["sex_label"] = df["성별코드"].map(SEX_LABELS).fillna("미상")
    df["edu_label"] = df["교육정도_학력구분코드"].map(EDU_LABELS).fillna("미상")
    df["field_label"] = df["교육정도_계열코드"].map(FIELD_LABELS).fillna("미상")
    df["school_status_label"] = df["교육정도_수학구분코드"].map(SCHOOL_STATUS_LABELS).fillna("미상")
    df["age_group_label"] = df["age_group"].map(AGE_GROUP_LABELS).fillna("미상")
    df["post_school_job_count_label"] = (
        df["졸업및중퇴후취업횟수코드"].map(POST_SCHOOL_JOB_COUNT_LABELS).fillna("미상")
    )
    df["unemp_duration_label"] = (
        df["미취업_퇴직후미취업기간구분코드"].map(UNEMP_DURATION_LABELS).fillna("미상")
    )
    df["unemp_activity_label"] = (
        df["미취업_주요활동상태코드"].map(UNEMP_ACTIVITY_LABELS).fillna("미상")
    )
    df["previous_occ_label"] = df["이전직장_8차직업대분류코드"].map(OCC_LABELS).fillna("없음/비해당")
    df["previous_status_label"] = (
        df["이전직장_종사상지위코드"].map(PREVIOUS_JOB_STATUS_LABELS).fillna("미상")
    )
    df["quit_reason_label"] = df["이전직장사항_이직사유코드"].map(QUIT_REASON_LABELS).fillna("기타/코드값")

    df["school_deferment"] = df["교육정도_수학구분코드"].isin(["2", "4"])
    df["school_leaver"] = df["교육정도_수학구분코드"].isin(["1", "3", "5"])
    df["dropout"] = df["교육정도_수학구분코드"].eq("3")
    df["graduated"] = df["교육정도_수학구분코드"].eq("1")
    df["highschool_or_less"] = df["교육정도_학력구분코드"].isin(["1", "2", "3"])
    df["tertiary"] = df["교육정도_학력구분코드"].isin(["4", "5", "6"])
    df["female"] = df["성별코드"].eq("2")
    df["age_25_29"] = df["age"].between(25, 29)

    df["no_post_school_job"] = df["졸업및중퇴후취업횟수코드"].eq("1")
    df["has_post_school_job"] = df["졸업및중퇴후취업횟수코드"].isin(["2", "3", "4", "5"])
    df["four_or_more_jobs"] = df["졸업및중퇴후취업횟수코드"].eq("5")
    df["prior_work"] = df["이전직장사항_전직유무"].eq("1") | df["has_post_school_job"]
    df["previous_job_within_1yr"] = df["이전직장사항_퇴직기간1년미만여부"].eq("1")
    df["previous_job_older_than_1yr"] = df["이전직장사항_퇴직기간1년미만여부"].eq("2")

    school_exit_month = df["가장최근학교_졸업및중퇴연월"].map(month_index)
    first_start_month = df["첫직장취업연월"].map(month_index)
    first_end_month = df["첫직장이직연월"].map(month_index)
    survey_month = df["조사연월"].map(month_index)
    df["months_since_school_exit"] = survey_month - school_exit_month
    df["months_to_first_job"] = first_start_month - school_exit_month
    df["first_job_tenure_months"] = first_end_month - first_start_month
    for col in ["months_since_school_exit", "months_to_first_job", "first_job_tenure_months"]:
        df.loc[df[col].lt(0), col] = np.nan

    df["short_first_job_12m"] = df["first_job_tenure_months"].between(0, 12, inclusive="both")
    df["very_short_first_job_6m"] = df["first_job_tenure_months"].between(0, 6, inclusive="both")
    df["delayed_first_job_2y"] = df["months_to_first_job"].gt(24)
    df["long_since_school_exit_3y"] = df["months_since_school_exit"].ge(36)

    df["want_job"] = df["기타활동사항_취업희망여부"].eq("1")
    df["do_not_want_job"] = df["기타활동사항_취업희망여부"].eq("2")
    df["job_possible"] = df["기타활동사항_취업가능성유무"].eq("1")
    df["searched_last_year"] = df["기타활동사항_지난1년내구직활동유무"].eq("1")
    df["searched_last_4w"] = df["구직사항_지난4주내구직여부"].eq("1")
    df["not_searching_last_4w"] = ~df["searched_last_4w"]
    df["employment_desire_possible"] = df["want_job"] & df["job_possible"]

    df["long_nonemployment_3y"] = df["미취업_퇴직후미취업기간구분코드"].eq("5")
    df["less_than_1y_nonemployment"] = df["미취업_퇴직후미취업기간구분코드"].isin(["1", "2"])
    df["simply_time"] = df["미취업_주요활동상태코드"].eq("3")
    df["low_activity"] = df["simply_time"] & df["do_not_want_job"]

    df["exam_prep"] = df["직업교육및직장체험_취업관련시험준비여부"].eq("1")
    df["job_training_received"] = df["직업교육및직장체험_직업교육수혜구분코드"].isin(["2", "3", "4"])
    df["work_experience_while_school"] = df["직업교육및직장체험_재학휴학중직장체험여부"].eq("1")
    df["exam_or_search"] = df["exam_prep"] | df["searched_last_year"] | df["employment_desire_possible"]

    df["poor_condition_exit"] = df["이전직장사항_이직사유코드"].eq("6")
    df["personal_family_exit"] = df["이전직장사항_이직사유코드"].eq("1")
    df["temporary_seasonal_exit"] = df["이전직장사항_이직사유코드"].eq("9")
    df["nonregular_previous_job"] = df["이전직장_종사상지위코드"].isin(["2", "3"])
    df["part_time_first_job"] = df["첫직장_임금근로자_전일제시간제여부"].eq("2")

    df["previous_occ_ai_score"] = df["이전직장_8차직업대분류코드"].map(AI_OCC_SCORE).astype(float)
    df["ai_direct_previous_job"] = df["previous_occ_ai_score"].ge(70)
    df["ai_broad_previous_job"] = df["previous_occ_ai_score"].ge(55)

    conditions = [
        df["school_deferment"],
        df["school_leaver"] & df["no_post_school_job"],
        df["short_first_job_12m"],
        df["previous_job_within_1yr"],
        df["long_nonemployment_3y"] | df["low_activity"],
        df["prior_work"],
    ]
    choices = PRIMARY_ORDER[:-1]
    df["primary_class"] = np.select(conditions, choices, default=PRIMARY_ORDER[-1])
    df["primary_class"] = pd.Categorical(df["primary_class"], categories=PRIMARY_ORDER, ordered=True)
    return df


def wsum(df: pd.DataFrame, mask: pd.Series | None = None) -> float:
    if mask is None:
        return float(df["weight_thousand"].sum())
    return float(df.loc[mask.fillna(False), "weight_thousand"].sum())


def wpct(df: pd.DataFrame, mask: pd.Series, base: pd.Series | None = None) -> float:
    if base is None:
        base = pd.Series(True, index=df.index)
    denom = wsum(df, base)
    return wsum(df, base & mask) / denom * 100 if denom else np.nan


def summarize_group(df: pd.DataFrame, group_cols: list[str], total_within: list[str] | None = None) -> pd.DataFrame:
    out = (
        df.groupby(group_cols, dropna=False, observed=False)
        .agg(weighted_thousand=("weight_thousand", "sum"), unweighted_n=("weight_thousand", "size"))
        .reset_index()
    )
    if total_within is None:
        denom = out["weighted_thousand"].sum()
        out["share_pct"] = out["weighted_thousand"] / denom * 100 if denom else np.nan
    else:
        out["share_pct"] = (
            out["weighted_thousand"]
            / out.groupby(total_within, observed=False)["weighted_thousand"].transform("sum")
            * 100
        )
    return out.sort_values(group_cols)


def profile_by_class(df: pd.DataFrame) -> pd.DataFrame:
    rows = []
    for cls in PRIMARY_ORDER:
        g = df[df["primary_class"].astype(str).eq(cls)]
        base = pd.Series(True, index=g.index)
        rows.append(
            {
                "primary_class": cls,
                "weighted_thousand": wsum(g),
                "share_pct": wsum(g) / wsum(df) * 100 if wsum(df) else np.nan,
                "unweighted_n": len(g),
                "female_pct": wpct(g, g["female"], base),
                "age_25_29_pct": wpct(g, g["age_25_29"], base),
                "highschool_or_less_pct": wpct(g, g["highschool_or_less"], base),
                "tertiary_pct": wpct(g, g["tertiary"], base),
                "dropout_pct": wpct(g, g["dropout"], base),
                "prior_work_pct": wpct(g, g["prior_work"], base),
                "previous_job_within_1yr_pct": wpct(g, g["previous_job_within_1yr"], base),
                "want_job_pct": wpct(g, g["want_job"], base),
                "job_possible_pct": wpct(g, g["job_possible"], base),
                "searched_last_year_pct": wpct(g, g["searched_last_year"], base),
                "exam_or_search_pct": wpct(g, g["exam_or_search"], base),
                "simply_time_pct": wpct(g, g["simply_time"], base),
                "long_nonemployment_3y_pct": wpct(g, g["long_nonemployment_3y"], base),
                "ai_direct_previous_job_pct": wpct(g, g["ai_direct_previous_job"], base),
                "ai_broad_previous_job_pct": wpct(g, g["ai_broad_previous_job"], base),
                "poor_condition_exit_pct": wpct(g, g["poor_condition_exit"], base),
                "temporary_seasonal_exit_pct": wpct(g, g["temporary_seasonal_exit"], base),
            }
        )
    return pd.DataFrame(rows)


TAG_DEFS = {
    "재학·휴학 유예": "school_deferment",
    "학교이탈": "school_leaver",
    "첫 일자리 미진입": "no_post_school_job",
    "첫 일자리 1년 이하 이탈": "short_first_job_12m",
    "최근 1년 내 직장 이탈": "previous_job_within_1yr",
    "3년 이상 미취업": "long_nonemployment_3y",
    "그냥 시간 보냄": "simply_time",
    "취업희망·취업가능": "employment_desire_possible",
    "최근 1년 구직·시험·취업가능": "exam_or_search",
    "직업훈련 경험": "job_training_received",
    "재학·휴학 중 직장체험": "work_experience_while_school",
    "AI 직접노출 이전직업": "ai_direct_previous_job",
    "AI 광의노출 이전직업": "ai_broad_previous_job",
    "근로여건 불만족 퇴직": "poor_condition_exit",
    "임시·계절 일자리 종료": "temporary_seasonal_exit",
    "이전 일자리 비정규": "nonregular_previous_job",
}


def profile_tags(df: pd.DataFrame) -> pd.DataFrame:
    rows = []
    for tag, col in TAG_DEFS.items():
        g = df[df[col]]
        rows.append(
            {
                "tag": tag,
                "weighted_thousand": wsum(g),
                "share_pct": wsum(g) / wsum(df) * 100 if wsum(df) else np.nan,
                "unweighted_n": len(g),
                "female_pct": wpct(g, g["female"]),
                "age_25_29_pct": wpct(g, g["age_25_29"]),
                "prior_work_pct": wpct(g, g["prior_work"]),
                "want_job_pct": wpct(g, g["want_job"]),
                "simply_time_pct": wpct(g, g["simply_time"]),
            }
        )
    return pd.DataFrame(rows).sort_values("weighted_thousand", ascending=False)


def top_policy_cells(df: pd.DataFrame) -> pd.DataFrame:
    grouped = (
        df.groupby(["primary_class", "age_group_label", "edu_label", "school_status_label"], observed=False)
        .agg(weighted_thousand=("weight_thousand", "sum"), unweighted_n=("weight_thousand", "size"))
        .reset_index()
    )
    grouped = grouped[grouped["weighted_thousand"].gt(0)].copy()
    grouped["share_pct"] = grouped["weighted_thousand"] / wsum(df) * 100
    return grouped.sort_values("weighted_thousand", ascending=False).head(30)


def codebook_counts(df: pd.DataFrame) -> pd.DataFrame:
    specs = [
        ("교육정도_수학구분코드", SCHOOL_STATUS_LABELS),
        ("졸업및중퇴후취업횟수코드", POST_SCHOOL_JOB_COUNT_LABELS),
        ("미취업_퇴직후미취업기간구분코드", UNEMP_DURATION_LABELS),
        ("미취업_주요활동상태코드", UNEMP_ACTIVITY_LABELS),
        ("이전직장_8차직업대분류코드", OCC_LABELS),
        ("이전직장_종사상지위코드", PREVIOUS_JOB_STATUS_LABELS),
        ("이전직장사항_이직사유코드", QUIT_REASON_LABELS),
    ]
    rows = []
    for col, labels in specs:
        grouped = df.groupby(col, dropna=False)["weight_thousand"].sum().reset_index()
        grouped.columns = ["code", "weighted_thousand"]
        grouped["variable"] = col
        grouped["label"] = grouped["code"].map(labels).fillna("기타/미상")
        grouped["share_pct"] = grouped["weighted_thousand"] / wsum(df) * 100
        rows.append(grouped[["variable", "code", "label", "weighted_thousand", "share_pct"]])
    return pd.concat(rows, ignore_index=True).sort_values(["variable", "weighted_thousand"], ascending=[True, False])


def fmt(value: float, digits: int = 1) -> str:
    return f"{float(value):,.{digits}f}"


def write_report(latest: pd.DataFrame, yearly_classes: pd.DataFrame, class_profiles: pd.DataFrame, tags: pd.DataFrame, cells: pd.DataFrame) -> None:
    total = wsum(latest)
    top_classes = class_profiles[class_profiles["weighted_thousand"].gt(0)].sort_values(
        "weighted_thousand", ascending=False
    )
    top_tags = tags.head(10)
    lines = [
        "# 쉬었음 청년 상세 분류: 전환단계·재진입·저활동 태그",
        "",
        "## 분석 범위",
        "",
        "- 원자료: 통계청 5월 청년층 부가조사 마이크로데이터 2016-2025년.",
        "- 대상: 만 15-29세, 경제활동상태 비경제활동, 지난주 주요활동상태 `쉬었음`.",
        "- 가중값 해석: `가중값 / 1,000 = 명`; 표의 인원은 천 명.",
        "- 분류 방식: 합계가 100%가 되는 `주유형`과 여러 개가 동시에 붙는 `정책 태그`를 함께 산출.",
        "",
        "## 2025년 주유형 분해",
        "",
        f"2025년 쉬었음 청년은 {fmt(total)}천 명이다. 주유형으로 보면 가장 큰 집단은 다음과 같다.",
        "",
        "|순위|주유형|천 명|비중|핵심 해석|",
        "|---:|---|---:|---:|---|",
    ]
    interpretations = {
        "A. 재학·휴학 유예형": "아직 학교와 완전히 분리되지 않은 대기·유예 상태.",
        "B. 학교이탈 후 첫 일자리 미진입형": "졸업·중퇴·수료 이후 첫 고용문턱을 넘지 못한 집단.",
        "C. 첫 일자리 1년 이하 단기이탈형": "첫 직장이 경력 신호나 숙련 축적이 되기 전에 종료된 집단.",
        "D. 최근 1년 내 직장 이탈형": "최근 직장에서 나왔지만 바로 재진입하지 못한 집단.",
        "E. 장기 비활동·저활동형": "3년 이상 미취업 또는 그냥 시간 보냄이 결합된 낮은 활동 집단.",
        "F. 경력 있음·재진입 보류형": "일 경험은 있으나 현재는 구직·재진입이 멈춘 집단.",
        "G. 기타·정보부족형": "관측 변수만으로 전환단계를 특정하기 어려운 잔여 집단.",
    }
    for rank, (_, row) in enumerate(top_classes.iterrows(), start=1):
        lines.append(
            f"|{rank}|{row.primary_class}|{fmt(row.weighted_thousand)}|{fmt(row.share_pct)}%|"
            f"{interpretations.get(row.primary_class, '')}|"
        )
    lines.extend(
        [
            "",
            "## 주유형별 위험 프로필",
            "",
            "|주유형|여성|25-29세|고졸 이하|일 경험|취업희망|그냥 시간 보냄|AI 노출 이전직업|",
            "|---|---:|---:|---:|---:|---:|---:|---:|",
        ]
    )
    for _, row in class_profiles[class_profiles["weighted_thousand"].gt(0)].iterrows():
        lines.append(
            f"|{row.primary_class}|{fmt(row.female_pct)}%|{fmt(row.age_25_29_pct)}%|"
            f"{fmt(row.highschool_or_less_pct)}%|{fmt(row.prior_work_pct)}%|"
            f"{fmt(row.want_job_pct)}%|{fmt(row.simply_time_pct)}%|"
            f"{fmt(row.ai_broad_previous_job_pct)}%|"
        )

    lines.extend(
        [
            "",
            "## 중첩 정책 태그",
            "",
            "주유형은 합계가 100%가 되도록 한 번만 배정한 값이다. 실제 정책 설계에는 중첩 태그가 더 중요하다.",
            "",
            "|태그|천 명|비중|25-29세|일 경험|취업희망|",
            "|---|---:|---:|---:|---:|---:|",
        ]
    )
    for _, row in top_tags.iterrows():
        lines.append(
            f"|{row.tag}|{fmt(row.weighted_thousand)}|{fmt(row.share_pct)}%|"
            f"{fmt(row.age_25_29_pct)}%|{fmt(row.prior_work_pct)}%|{fmt(row.want_job_pct)}%|"
        )

    lines.extend(
        [
            "",
            "## 상위 정책 셀",
            "",
            "|순위|주유형|연령|학력|재학상태|천 명|비중|",
            "|---:|---|---|---|---|---:|---:|",
        ]
    )
    for rank, (_, row) in enumerate(cells.head(12).iterrows(), start=1):
        lines.append(
            f"|{rank}|{row.primary_class}|{row.age_group_label}|{row.edu_label}|"
            f"{row.school_status_label}|{fmt(row.weighted_thousand)}|{fmt(row.share_pct)}%|"
        )

    trend = yearly_classes[yearly_classes["primary_class"].isin(PRIMARY_ORDER)].copy()
    latest_years = trend[trend["year"].isin([2016, 2020, 2025])]
    lines.extend(
        [
            "",
            "## 시계열에서 읽을 점",
            "",
            "|연도|주유형|천 명|해당 연도 내 비중|",
            "|---:|---|---:|---:|",
        ]
    )
    for _, row in latest_years.iterrows():
        lines.append(
            f"|{int(row.year)}|{row.primary_class}|{fmt(row.weighted_thousand)}|{fmt(row.share_pct)}%|"
        )

    lines.extend(
        [
            "",
            "## 해석",
            "",
            "1. `쉬었음`은 하나의 상태명이지만 내부적으로는 학교와 아직 연결된 유예형, 첫 일자리 문턱을 넘지 못한 미진입형, 첫 직장을 짧게 끝낸 단기이탈형, 최근 이탈 후 재진입하지 못한 집단, 장기 저활동형으로 갈라진다.",
            "2. AI 시대 위험은 주유형마다 다르다. 첫 일자리 미진입형은 초급 과업 축소에, 단기이탈형과 최근이탈형은 경력 신호 약화와 재매칭 비용 상승에, 장기 저활동형은 생활리듬·관계망 회복 실패에 더 취약하다.",
            "3. 정책은 `쉬었음 청년` 전체에 같은 교육쿠폰을 주는 방식보다, 주유형과 태그를 결합해 개입 순서를 달리해야 한다.",
            "",
            "## 산출 파일",
            "",
            "- `primary_classes_yearly.csv`: 2016-2025년 주유형별 규모.",
            "- `class_profiles_2025.csv`: 2025년 주유형별 상세 프로필.",
            "- `policy_tags_2025.csv`: 중첩 정책 태그 규모와 특성.",
            "- `top_policy_cells_2025.csv`: 연령·학력·재학상태를 결합한 상위 정책 셀.",
            "- `codebook_counts_2025.csv`: 주요 코드값 분포와 라벨.",
        ]
    )
    (OUT / "detailed_classification_report.md").write_text("\n".join(lines), encoding="utf-8")


def main() -> None:
    data = add_features(read_sources())
    target = data[data["is_youth"] & data["is_inactive"] & data["is_resting"]].copy()
    latest = target[target["year"].eq(2025)].copy()

    yearly_classes = summarize_group(target, ["year", "primary_class"], total_within=["year"])
    class_profiles = profile_by_class(latest)
    tags = profile_tags(latest)
    cells = top_policy_cells(latest)
    code_counts = codebook_counts(latest)
    class_by_age_edu = summarize_group(
        latest, ["primary_class", "age_group_label", "edu_label"], total_within=["primary_class"]
    )
    class_by_previous_occ = summarize_group(
        latest, ["primary_class", "previous_occ_label"], total_within=["primary_class"]
    )

    outputs = {
        "primary_classes_yearly": yearly_classes,
        "class_profiles_2025": class_profiles,
        "policy_tags_2025": tags,
        "top_policy_cells_2025": cells,
        "codebook_counts_2025": code_counts,
        "class_by_age_edu_2025": class_by_age_edu,
        "class_by_previous_occ_2025": class_by_previous_occ,
    }
    for name, table in outputs.items():
        table.to_csv(OUT / f"{name}.csv", index=False, encoding="utf-8-sig")
        table.to_json(OUT / f"{name}.json", orient="records", force_ascii=False, indent=2)

    metadata = {
        "source_dirs": [str(path) for path in SOURCE_DIRS],
        "population_scope": "Age 15-29, economically inactive, main activity status code 11 (resting), May youth supplementary labor-force survey.",
        "weight_note": "가중값 / 1,000 = persons; output count columns are thousand persons.",
        "primary_class_order": PRIMARY_ORDER,
        "tag_definitions": TAG_DEFS,
        "notes": [
            "Primary classes are mutually exclusive and assigned by hierarchy.",
            "Policy tags are non-mutually-exclusive and should be used for intervention design.",
            "Some activity-code labels are retained as code labels where no separate codebook was present in the source folder.",
        ],
    }
    (OUT / "metadata.json").write_text(json.dumps(metadata, ensure_ascii=False, indent=2), encoding="utf-8")
    write_report(latest, yearly_classes, class_profiles, tags, cells)

    print("Wrote", OUT)
    print(class_profiles[["primary_class", "weighted_thousand", "share_pct"]].round(2).to_string(index=False))
    print()
    print(tags[["tag", "weighted_thousand", "share_pct"]].head(12).round(2).to_string(index=False))


if __name__ == "__main__":
    main()
