"""
도전과제 시스템 — 357개 자동 평가.

설계:
- 각 도전과제는 dataclass `Achievement` + 평가 함수 (db) -> (progress, target).
- 평가자는 5분 throttle로 호출되어 미획득 도전과제만 검사.
- 신규 잠금 해제 시 main_window의 `notify_achievement_unlocked` 시그널 emit.
- 진행도(progress)는 부분 달성 표시용 — UI 게이지에 활용.

데이터 소스:
- work_records: 출근/퇴근 기록
- overtime_bank, overtime_usage: 적립/사용
- leave_records: 연차
- break_records: 외출/식사
- holidays: 공휴일
- settings: 사용자 메타 + 뷰 카운터
- notification_log: 휴식 권고 카운트
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date, datetime, timedelta
from typing import Callable, Optional, List, Tuple

# === 등급 상수 ===
TIER_BRONZE = 'bronze'
TIER_SILVER = 'silver'
TIER_GOLD = 'gold'
TIER_PLATINUM = 'platinum'
TIER_LEGEND = 'legend'

# === 카테고리 ===
CAT_STREAK = 'streak'
CAT_PUNCTUAL = 'punctual'
CAT_BALANCE = 'balance'
CAT_OT_BANK = 'ot_bank'
CAT_OT_USE = 'ot_use'
CAT_LEAVE = 'leave'
CAT_HEALTH = 'health'
CAT_SPECIAL_DAY = 'special_day'
CAT_PATTERN = 'pattern'
CAT_MILESTONE = 'milestone'
CAT_SEASON = 'season'
CAT_TIME_SLOT = 'time_slot'
CAT_MEAL = 'meal'
CAT_BREAK = 'break_use'
CAT_SETTINGS = 'settings'
CAT_STATS = 'stats'
CAT_SECRET = 'secret'
CAT_KOREA = 'korea'
CAT_AMBITION = 'ambition'
CAT_META = 'meta'


@dataclass
class Achievement:
    """도전과제 정의.

    평가 함수 `evaluator`는 `(db) -> (progress, target)` 시그니처.
    progress >= target이면 잠금 해제. progress가 0이면 미시작.
    """
    code: str
    name: str
    description: str
    category: str
    tier: str
    badge_icon: str
    is_secret: bool = False
    target: int = 1
    evaluator: Optional[Callable] = field(default=None, repr=False)


# ============================================================
# 평가 헬퍼
# ============================================================

def _count_work_records(db) -> int:
    """전체 work_records 수 (clock_in이 있는 모든 행)."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT COUNT(*) FROM work_records")
        return cur.fetchone()[0]


def _count_clocked_out(db) -> int:
    """퇴근까지 마친 work_records 수."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT COUNT(*) FROM work_records WHERE clock_out IS NOT NULL")
        return cur.fetchone()[0]


def _consecutive_workdays(db) -> int:
    """오늘 또는 마지막 출근일 기준 연속 영업일 출근 수.

    영업일 = 토/일이 아니고 holidays 테이블에 없는 날.
    """
    today = date.today()
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT date FROM work_records ORDER BY date DESC LIMIT 1")
        row = cur.fetchone()
        if not row:
            return 0
        last = datetime.strptime(row[0], '%Y-%m-%d').date()
        if (today - last).days > 3:
            return 0  # 끊김
        # holidays 셋
        cur.execute("SELECT date FROM holidays")
        holidays = {r[0] for r in cur.fetchall()}
        cur.execute("SELECT date FROM work_records")
        worked = {r[0] for r in cur.fetchall()}

    streak = 0
    d = last
    while True:
        is_workday = d.weekday() < 5 and d.isoformat() not in holidays
        if is_workday:
            if d.isoformat() in worked:
                streak += 1
            else:
                break
        d -= timedelta(days=1)
        if streak > 1000:  # safety
            break
    return streak


def _consecutive_calendar_days(db) -> int:
    """달력일 기준 연속 출근 수 (주말 포함)."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT date FROM work_records ORDER BY date DESC")
        dates = [datetime.strptime(r[0], '%Y-%m-%d').date() for r in cur.fetchall()]
    if not dates:
        return 0
    streak = 1
    for i in range(1, len(dates)):
        if (dates[i-1] - dates[i]).days == 1:
            streak += 1
        else:
            break
    return streak


def _ot_balance_minutes(db) -> int:
    """현재 연장근무 잔액 (분)."""
    return db.get_total_overtime_balance()


def _ot_total_earned(db) -> int:
    """누적 적립 분."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT COALESCE(SUM(earned_minutes), 0) FROM overtime_bank")
        return cur.fetchone()[0]


def _ot_total_used(db) -> int:
    """누적 사용 분."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT COALESCE(SUM(used_minutes), 0) FROM overtime_usage")
        return cur.fetchone()[0]


def _count_punctual_clockouts(db) -> int:
    """정시 퇴근 (overtime_minutes <= 0) 횟수."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records
            WHERE clock_out IS NOT NULL AND COALESCE(overtime_minutes, 0) <= 0
        """)
        return cur.fetchone()[0]


def _count_clock_in_before(db, hour: int, minute: int = 0) -> int:
    """특정 시각 이전 출근 횟수."""
    threshold = f"{hour:02d}:{minute:02d}:00"
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records WHERE clock_in < ?
        """, (threshold,))
        return cur.fetchone()[0]


def _count_clock_in_in_range(db, start_hh: int, end_hh: int) -> int:
    """[start_hh:00, end_hh:00) 시간대 출근 횟수."""
    s = f"{start_hh:02d}:00:00"
    e = f"{end_hh:02d}:00:00"
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records WHERE clock_in >= ? AND clock_in < ?
        """, (s, e))
        return cur.fetchone()[0]


def _count_clock_out_after(db, hour: int) -> int:
    """특정 시각(시) 이후 퇴근 횟수. 자정 이후는 hour=24로 별도 처리."""
    threshold = f"{hour:02d}:00:00"
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records
            WHERE clock_out IS NOT NULL AND clock_out >= ?
        """, (threshold,))
        return cur.fetchone()[0]


def _count_clock_out_after_midnight(db) -> int:
    """자정 이후 ~ 오전 퇴근 횟수 (clock_out < clock_in인 경우, 익일).
    같은 날짜 내에서 clock_out HH:MM:SS가 clock_in보다 작으면 자정 넘긴 것."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records
            WHERE clock_out IS NOT NULL AND clock_out < clock_in
        """)
        return cur.fetchone()[0]


def _count_weekend_clockins(db) -> int:
    """토/일 출근 횟수."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records
            WHERE CAST(strftime('%w', date) AS INTEGER) IN (0, 6)
        """)
        return cur.fetchone()[0]


def _count_holiday_clockins(db) -> int:
    """공휴일 출근 (holidays 테이블에 있는 날)."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records w
            WHERE EXISTS (SELECT 1 FROM holidays h WHERE h.date = w.date)
        """)
        return cur.fetchone()[0]


def _has_clockin_on(db, mm_dd: str) -> bool:
    """특정 MM-DD에 출근 기록 있는지."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT 1 FROM work_records WHERE strftime('%m-%d', date) = ? LIMIT 1
        """, (mm_dd,))
        return cur.fetchone() is not None


def _has_punctual_clockout_on(db, mm_dd: str) -> bool:
    """특정 MM-DD에 정시 퇴근."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT 1 FROM work_records
            WHERE strftime('%m-%d', date) = ?
            AND clock_out IS NOT NULL AND COALESCE(overtime_minutes, 0) <= 0
            LIMIT 1
        """, (mm_dd,))
        return cur.fetchone() is not None


def _count_lunch_registrations(db) -> int:
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT COUNT(*) FROM work_records WHERE lunch_break = 1")
        return cur.fetchone()[0]


def _count_dinner_registrations(db) -> int:
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT COUNT(*) FROM work_records WHERE dinner_break = 1")
        return cur.fetchone()[0]


def _count_break_records_type(db, break_type: str = 'break') -> int:
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT COUNT(*) FROM break_records WHERE break_type = ?", (break_type,))
        return cur.fetchone()[0]


def _count_leave_records(db, leave_type: str = None) -> int:
    with db._conn() as conn:
        cur = conn.cursor()
        if leave_type:
            cur.execute("SELECT COUNT(*) FROM leave_records WHERE leave_type = ?", (leave_type,))
        else:
            cur.execute("SELECT COUNT(*) FROM leave_records")
        return cur.fetchone()[0]


def _has_leave_with_days(db, days_value: float) -> bool:
    """특정 days 값(0.5, 0.25 등)의 연차 사용 여부."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT 1 FROM leave_records WHERE days = ? LIMIT 1", (days_value,))
        return cur.fetchone() is not None


def _consecutive_leave_days(db) -> int:
    """가장 긴 연속 연차 사용 일수."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT date FROM leave_records ORDER BY date")
        dates = [datetime.strptime(r[0], '%Y-%m-%d').date() for r in cur.fetchall()]
    if not dates:
        return 0
    max_streak = 1
    cur_streak = 1
    for i in range(1, len(dates)):
        if (dates[i] - dates[i-1]).days == 1:
            cur_streak += 1
            max_streak = max(max_streak, cur_streak)
        else:
            cur_streak = 1
    return max_streak


def _setting_int(db, key: str, default: int = 0) -> int:
    return db.get_setting_int(key, default)


def _setting_str(db, key: str, default: str = '') -> str:
    return db.get_setting(key, default) or default


def _days_since_first_work(db) -> int:
    """첫 work_records로부터 오늘까지 경과 일수."""
    hire = _setting_str(db, 'hire_date', '')
    if not hire:
        return 0
    try:
        d = datetime.strptime(hire, '%Y-%m-%d').date()
        return (date.today() - d).days
    except ValueError:
        return 0


def _has_clockin_on_date(db, target_date: date) -> bool:
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT 1 FROM work_records WHERE date = ? LIMIT 1",
                     (target_date.isoformat(),))
        return cur.fetchone() is not None


def _count_overtime_days(db, min_minutes: int = 1) -> int:
    """야근(overtime_minutes >= min_minutes)한 일수."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records WHERE overtime_minutes >= ?
        """, (min_minutes,))
        return cur.fetchone()[0]


def _count_clockouts_at_minute(db, minute: int) -> int:
    """퇴근 시각의 분 자릿수가 특정 값인 횟수."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records
            WHERE clock_out IS NOT NULL
            AND CAST(strftime('%M', clock_out) AS INTEGER) = ?
        """, (minute,))
        return cur.fetchone()[0]


def _count_in_year_month(db, year: int, month: int) -> int:
    """특정 연-월 출근 일수."""
    prefix = f"{year:04d}-{month:02d}"
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records
            WHERE date LIKE ? || '%'
        """, (prefix,))
        return cur.fetchone()[0]


def _workdays_in_year_month(year: int, month: int) -> int:
    """해당 월의 영업일 수 (토/일 제외, 공휴일은 무시)."""
    from calendar import monthrange
    last = monthrange(year, month)[1]
    count = 0
    for d in range(1, last + 1):
        if date(year, month, d).weekday() < 5:
            count += 1
    return count


# ============================================================
# 도전과제 정의 (357개)
# ============================================================

def _make_streak_eval(target_days: int, business_only: bool = True):
    def _eval(db):
        cur = (_consecutive_workdays(db) if business_only
                else _consecutive_calendar_days(db))
        return min(cur, target_days), target_days
    return _eval


def _make_count_eval(getter, target: int):
    def _eval(db):
        cur = getter(db)
        return min(cur, target), target
    return _eval


def _bool_eval(condition_fn):
    """True/False 조건 → (1, 1) 또는 (0, 1)."""
    def _eval(db):
        return (1, 1) if condition_fn(db) else (0, 1)
    return _eval


# ---- 1. 출근 streak (24개 — 22번 거북이 제거) ----
_STREAK_DEFS = [
    # (code, name, desc, target, evaluator, tier, icon)
    ('streak_first', '첫걸음', '첫 출근 기록', 1,
        _bool_eval(lambda db: _count_work_records(db) >= 1), TIER_BRONZE, '👋'),
    ('streak_3', '뿌리내림', '3일 연속 영업일 출근', 3,
        _make_streak_eval(3), TIER_BRONZE, '🌱'),
    ('streak_5', '첫 주 완주', '5 영업일 연속 출근', 5,
        _make_streak_eval(5), TIER_SILVER, '📅'),
    ('streak_7_cal', '7일 연속', '주말 포함 7일 연속 출근', 7,
        _make_streak_eval(7, business_only=False), TIER_SILVER, '🔥'),
    ('streak_10', '2주 연속', '10 영업일 연속 출근', 10,
        _make_streak_eval(10), TIER_SILVER, '💪'),
    ('streak_22', '한 달 개근', '한 달 영업일 100% 출근 (22일)', 22,
        _make_streak_eval(22), TIER_GOLD, '🏔️'),
    ('streak_50', '50일 연속', '50 영업일 연속 출근', 50,
        _make_streak_eval(50), TIER_GOLD, '🎯'),
    ('streak_100', '100일 연속', '100 영업일 연속 출근', 100,
        _make_streak_eval(100), TIER_PLATINUM, '💎'),
    ('streak_quarter', '분기 완주', '약 65 영업일 (3개월)', 65,
        _make_streak_eval(65), TIER_PLATINUM, '🏆'),
    ('streak_half_year', '반년 마라톤', '약 130 영업일 (6개월)', 130,
        _make_streak_eval(130), TIER_PLATINUM, '👑'),
    ('streak_year', '1년 풀 시즌', '약 260 영업일 (1년)', 260,
        _make_streak_eval(260), TIER_LEGEND, '🌟'),
    ('streak_200', '사이언스', '200 영업일 연속', 200,
        _make_streak_eval(200), TIER_LEGEND, '🌌'),
    ('streak_365_cal', '불사신', '365일 달력 연속', 365,
        _make_streak_eval(365, business_only=False), TIER_LEGEND, '🛡️'),
    ('streak_resilience', '회복력', '결근 후 다음날 즉시 출근 (자동: 달력 streak 깨진 후 재시작)', 1,
        _bool_eval(lambda db: _consecutive_workdays(db) >= 1
                    and _count_work_records(db) >= 5), TIER_BRONZE, '⚡'),
    ('streak_total_100', '누적 100회', '누적 출근 100회', 100,
        _make_count_eval(_count_work_records, 100), TIER_GOLD, '💼'),
    ('streak_total_500', '누적 500회', '누적 출근 500회', 500,
        _make_count_eval(_count_work_records, 500), TIER_PLATINUM, '🏛️'),
    ('streak_total_1000', '누적 1000회', '누적 출근 1000회', 1000,
        _make_count_eval(_count_work_records, 1000), TIER_LEGEND, '🎖️'),
]


def _count_weekday_clockins(db, weekday: int) -> int:
    """특정 요일(0=월 ... 6=일) 출근 횟수."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records
            WHERE CAST(strftime('%w', date) AS INTEGER) = ?
        """, (weekday,))
        return cur.fetchone()[0]


_STREAK_DEFS.extend([
    ('streak_monday_10', '월요일 정복', '월요일 10주 연속 출근', 10,
        _make_count_eval(lambda db: _count_weekday_clockins(db, 1), 10), TIER_SILVER, '🌅'),
    ('streak_friday_10', '금요일 무결', '금요일 10주 연속 출근', 10,
        _make_count_eval(lambda db: _count_weekday_clockins(db, 5), 10), TIER_SILVER, '🌒'),
])


# ---- 2. 시간 엄수 (19개 - 34/46 제거) ----
_PUNCTUAL_DEFS = [
    ('punc_before_8_1', '얼리버드', '08:00 이전 출근 1회', 1,
        _make_count_eval(lambda db: _count_clock_in_before(db, 8), 1), TIER_BRONZE, '🌄'),
    ('punc_before_8_10', '참새족', '08:00 이전 10회', 10,
        _make_count_eval(lambda db: _count_clock_in_before(db, 8), 10), TIER_SILVER, '🐦'),
    ('punc_before_8_30', '일찍 자고 일찍', '08:00 이전 30회', 30,
        _make_count_eval(lambda db: _count_clock_in_before(db, 8), 30), TIER_GOLD, '🌞'),
    ('punc_before_6_1', '새벽잠 없음', '06:00 이전 1회', 1,
        _make_count_eval(lambda db: _count_clock_in_before(db, 6), 1), TIER_GOLD, '🥱'),
    ('punc_before_6_10', '어둠을 가르는 자', '06:00 이전 10회', 10,
        _make_count_eval(lambda db: _count_clock_in_before(db, 6), 10), TIER_PLATINUM, '🌑'),
    ('punc_before_5', '새벽 챔피언', '05:00 이전 출근', 1,
        _make_count_eval(lambda db: _count_clock_in_before(db, 5), 1), TIER_LEGEND, '🌌'),
    ('punc_at_9', '9시 정각', '09:00 정각(±1분) 출근 1회', 1,
        _make_count_eval(lambda db: _count_clock_in_in_range_minute(db, 9, 0, 9, 2), 1),
        TIER_BRONZE, '🎯'),
    ('punc_at_9_5', '완벽한 9시', '09:00 정각(±1분) 5회', 5,
        _make_count_eval(lambda db: _count_clock_in_in_range_minute(db, 9, 0, 9, 2), 5),
        TIER_SILVER, '🏹'),
    ('punc_late_5min', '5분 늦음', '09:00~09:05 출근 1회 (자조)', 1,
        _make_count_eval(lambda db: _count_clock_in_in_range_minute(db, 9, 0, 9, 6), 1),
        TIER_BRONZE, '🛌'),
    ('punc_at_909', '운명의 시각', '09:09 출근 (시크릿)', 1,
        _make_count_eval(lambda db: _count_clock_in_in_range_minute(db, 9, 9, 9, 10), 1),
        TIER_GOLD, '🎰'),
]


def _count_clock_in_in_range_minute(db, sh: int, sm: int, eh: int, em: int) -> int:
    s = f"{sh:02d}:{sm:02d}:00"
    e = f"{eh:02d}:{em:02d}:00"
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records WHERE clock_in >= ? AND clock_in < ?
        """, (s, e))
        return cur.fetchone()[0]


# ---- 3. 워라밸·정시 퇴근 (8개 코어) ----
_BALANCE_DEFS = [
    ('bal_first_punct', '첫 칼퇴', '정시 퇴근 첫 달성', 1,
        _make_count_eval(_count_punctual_clockouts, 1), TIER_BRONZE, '🚪'),
    ('bal_punct_10', '칼퇴러', '정시 퇴근 10회', 10,
        _make_count_eval(_count_punctual_clockouts, 10), TIER_SILVER, '🎉'),
    ('bal_punct_30', '칼퇴 챔프', '정시 퇴근 30회', 30,
        _make_count_eval(_count_punctual_clockouts, 30), TIER_GOLD, '🏃'),
    ('bal_punct_100', '진정한 자유', '정시 퇴근 100회', 100,
        _make_count_eval(_count_punctual_clockouts, 100), TIER_LEGEND, '🏖️'),
    ('bal_punct_300', '워라밸 마스터', '정시 퇴근 300회', 300,
        _make_count_eval(_count_punctual_clockouts, 300), TIER_LEGEND, '🪐'),
]


# ---- 4. 연장근무 적립 ----
_OT_BANK_DEFS = [
    ('ot_first_30m', '첫 30분', '첫 연장 적립', 30,
        _make_count_eval(_ot_total_earned, 30), TIER_BRONZE, '💰'),
    ('ot_total_60m', '1시간 적금', '누적 1시간 적립', 60,
        _make_count_eval(_ot_total_earned, 60), TIER_BRONZE, '💵'),
    ('ot_total_5h', '5시간 적립', '누적 5시간', 300,
        _make_count_eval(_ot_total_earned, 300), TIER_SILVER, '🏦'),
    ('ot_total_10h', '10시간 적립', '누적 10시간', 600,
        _make_count_eval(_ot_total_earned, 600), TIER_SILVER, '💎'),
    ('ot_total_25h', '25시간 적립', '누적 25시간', 1500,
        _make_count_eval(_ot_total_earned, 1500), TIER_GOLD, '🏆'),
    ('ot_total_50h', '50시간 적립', '누적 50시간', 3000,
        _make_count_eval(_ot_total_earned, 3000), TIER_GOLD, '🎯'),
    ('ot_total_100h', '마라토너', '누적 100시간 (걱정 메시지)', 6000,
        _make_count_eval(_ot_total_earned, 6000), TIER_PLATINUM, '🏔️'),
    ('ot_total_200h', '워크홀릭 경고', '누적 200시간 (경고)', 12000,
        _make_count_eval(_ot_total_earned, 12000), TIER_PLATINUM, '🌑'),
    ('ot_total_300h', '위험 신호', '누적 300시간 (강한 경고)', 18000,
        _make_count_eval(_ot_total_earned, 18000), TIER_LEGEND, '⚠️'),
    ('ot_total_500h', '응급실 단골', '누적 500시간 (자조)', 30000,
        _make_count_eval(_ot_total_earned, 30000), TIER_LEGEND, '🚑'),
]


# ---- 5. 연장근무 사용 ----
_OT_USE_DEFS = [
    ('use_first', '첫 휴식', '적립 첫 사용', 1,
        _bool_eval(lambda db: _ot_total_used(db) > 0), TIER_BRONZE, '🛌'),
    ('use_total_5h', '선물 사용', '누적 5시간 사용', 300,
        _make_count_eval(_ot_total_used, 300), TIER_SILVER, '🎁'),
    ('use_total_25h', '휴식의 가치', '누적 25시간 사용', 1500,
        _make_count_eval(_ot_total_used, 1500), TIER_GOLD, '🛀'),
    ('use_total_50h', '회복 마스터', '누적 50시간 사용', 3000,
        _make_count_eval(_ot_total_used, 3000), TIER_GOLD, '🏖️'),
    ('use_total_100h', '마사지', '누적 100시간 사용', 6000,
        _make_count_eval(_ot_total_used, 6000), TIER_PLATINUM, '💆'),
]


# ---- 6. 연차 ----
_LEAVE_DEFS = [
    ('leave_first', '첫 연차', '첫 연차 사용', 1,
        _make_count_eval(_count_leave_records, 1), TIER_BRONZE, '🌴'),
    ('leave_half', '첫 반차', '0.5일 연차 사용', 1,
        _bool_eval(lambda db: _has_leave_with_days(db, 0.5)), TIER_BRONZE, '🍃'),
    ('leave_quarter', '시간 연차', '0.25일 연차 사용', 1,
        _bool_eval(lambda db: _has_leave_with_days(db, 0.25)), TIER_BRONZE, '⏱️'),
    ('leave_streak_3', '미니 휴가', '연속 3일 연차', 3,
        _make_count_eval(_consecutive_leave_days, 3), TIER_SILVER, '🏝️'),
    ('leave_streak_5', '본격 휴가', '연속 5일 연차', 5,
        _make_count_eval(_consecutive_leave_days, 5), TIER_GOLD, '🌅'),
    ('leave_streak_7', '장거리 휴가', '연속 7일 이상 연차', 7,
        _make_count_eval(_consecutive_leave_days, 7), TIER_PLATINUM, '🛬'),
    ('leave_total_10', '연차 10회', '연차 기록 10건', 10,
        _make_count_eval(_count_leave_records, 10), TIER_SILVER, '🌊'),
    ('leave_sick', '병가', 'sick 타입 연차 사용', 1,
        _make_count_eval(lambda db: _count_leave_records(db, 'sick'), 1),
        TIER_BRONZE, '🏥'),
]


# ---- 7. 식사 (점심/저녁) ----
_MEAL_DEFS = [
    ('meal_lunch_first', '첫 점심 등록', '점심 첫 토글', 1,
        _make_count_eval(_count_lunch_registrations, 1), TIER_BRONZE, '🍱'),
    ('meal_lunch_30', '점심 마스터', '점심 등록 30회', 30,
        _make_count_eval(_count_lunch_registrations, 30), TIER_SILVER, '🥢'),
    ('meal_lunch_100', '점심 챔프', '점심 등록 100회', 100,
        _make_count_eval(_count_lunch_registrations, 100), TIER_GOLD, '🍜'),
    ('meal_dinner_first', '첫 저녁 등록', '저녁 첫 토글', 1,
        _make_count_eval(_count_dinner_registrations, 1), TIER_BRONZE, '🍽️'),
    ('meal_dinner_10', '저녁 단골', '저녁 등록 10회 (경고)', 10,
        _make_count_eval(_count_dinner_registrations, 10), TIER_SILVER, '🍛'),
    ('meal_dinner_30', '야식 단골', '저녁 등록 30회 (경고)', 30,
        _make_count_eval(_count_dinner_registrations, 30), TIER_GOLD, '🌃'),
    ('meal_lunch_actual', '실측 점심', '실제 점심 시각 입력', 1,
        _make_count_eval(lambda db: _count_break_records_type(db, 'lunch'), 1),
        TIER_BRONZE, '⏱️'),
    ('meal_dinner_actual', '실측 저녁', '실제 저녁 시각 입력', 1,
        _make_count_eval(lambda db: _count_break_records_type(db, 'dinner'), 1),
        TIER_BRONZE, '⏰'),
]


# ---- 8. 외출 ----
_BREAK_DEFS = [
    ('break_first', '첫 외출', '첫 외출 시작', 1,
        _make_count_eval(lambda db: _count_break_records_type(db, 'break'), 1),
        TIER_BRONZE, '🚶'),
    ('break_10', '외출 챔프', '외출 10회', 10,
        _make_count_eval(lambda db: _count_break_records_type(db, 'break'), 10),
        TIER_SILVER, '🚪'),
    ('break_50', '산책러', '외출 50회', 50,
        _make_count_eval(lambda db: _count_break_records_type(db, 'break'), 50),
        TIER_GOLD, '🚶‍♂️'),
]


# ---- 9. 시간대별 ----
_TIME_SLOT_DEFS = [
    ('slot_in_06', '06시대 출근', '06:00-06:59 출근 1회', 1,
        _make_count_eval(lambda db: _count_clock_in_in_range(db, 6, 7), 1),
        TIER_BRONZE, '🌅'),
    ('slot_in_07', '07시대 출근', '07:00-07:59 출근 1회', 1,
        _make_count_eval(lambda db: _count_clock_in_in_range(db, 7, 8), 1),
        TIER_BRONZE, '🌄'),
    ('slot_in_08', '08시대 출근', '08:00-08:59 출근 1회', 1,
        _make_count_eval(lambda db: _count_clock_in_in_range(db, 8, 9), 1),
        TIER_BRONZE, '☀️'),
    ('slot_in_10', '10시대 출근', '10시대 출근 (지각/유연근무)', 1,
        _make_count_eval(lambda db: _count_clock_in_in_range(db, 10, 11), 1),
        TIER_BRONZE, '🕙'),
    ('slot_in_11', '11시대 출근', '11시대 출근 (자조)', 1,
        _make_count_eval(lambda db: _count_clock_in_in_range(db, 11, 12), 1),
        TIER_SILVER, '🕦'),
    ('slot_out_19', '19시대 퇴근', '19시대 퇴근 10회 (경고)', 10,
        _make_count_eval(lambda db: _count_clockouts_in_hour(db, 19), 10),
        TIER_SILVER, '🌆'),
    ('slot_out_20', '20시대 퇴근', '20시대 퇴근 10회 (경고)', 10,
        _make_count_eval(lambda db: _count_clockouts_in_hour(db, 20), 10),
        TIER_GOLD, '🌌'),
    ('slot_out_21', '21시대 퇴근', '21시대 퇴근 5회 (경고)', 5,
        _make_count_eval(lambda db: _count_clockouts_in_hour(db, 21), 5),
        TIER_GOLD, '🌑'),
    ('slot_out_22', '22시대 퇴근', '22시대 퇴근 1회 (경고)', 1,
        _make_count_eval(lambda db: _count_clockouts_in_hour(db, 22), 1),
        TIER_PLATINUM, '🦉'),
    ('slot_out_23', '23시대 퇴근', '23시대 퇴근 1회 (경고)', 1,
        _make_count_eval(lambda db: _count_clockouts_in_hour(db, 23), 1),
        TIER_PLATINUM, '🦇'),
    ('slot_midnight', '자정 퇴근', '자정 이후 퇴근 (경고)', 1,
        _make_count_eval(_count_clock_out_after_midnight, 1), TIER_LEGEND, '🌚'),
    ('slot_midnight_3', '올빼미 트리오', '자정 이후 퇴근 3회 (경고)', 3,
        _make_count_eval(_count_clock_out_after_midnight, 3), TIER_LEGEND, '🌌'),
]


def _count_clockouts_in_hour(db, hour: int) -> int:
    """clock_out이 hour시대(HH:00-HH:59)인 횟수. 자정 넘김 케이스 무시."""
    s = f"{hour:02d}:00:00"
    e = f"{hour+1:02d}:00:00" if hour < 23 else "23:59:59"
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM work_records
            WHERE clock_out IS NOT NULL AND clock_out >= ? AND clock_out < ?
            AND clock_out >= clock_in
        """, (s, e))
        return cur.fetchone()[0]


# ---- 10. 공휴일·주말 ----
_SPECIAL_DAY_DEFS = [
    ('weekend_1', '주말 출근 1회', '토/일 출근 1회', 1,
        _make_count_eval(_count_weekend_clockins, 1), TIER_SILVER, '🌃'),
    ('weekend_5', '주말 워커', '주말 출근 5회 (경고)', 5,
        _make_count_eval(_count_weekend_clockins, 5), TIER_GOLD, '🌑'),
    ('weekend_20', '진짜 워크홀릭', '주말 출근 20회 (강한 자조)', 20,
        _make_count_eval(_count_weekend_clockins, 20), TIER_PLATINUM, '💀'),
    ('holiday_1', '공휴일 출근', '한국 공휴일 출근 1회', 1,
        _make_count_eval(_count_holiday_clockins, 1), TIER_GOLD, '📆'),
    ('holiday_5', '공휴일 워커홀릭', '한국 공휴일 출근 5회 (경고)', 5,
        _make_count_eval(_count_holiday_clockins, 5), TIER_LEGEND, '⚠️'),
    ('day_christmas', '크리스마스 출근', '12/25 출근 (자조)', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '12-25')), TIER_GOLD, '🎄'),
    ('day_newyear', '신정 출근', '1/1 출근 (자조)', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '01-01')), TIER_GOLD, '🎊'),
    ('day_liberation', '광복절 출근', '8/15 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '08-15')), TIER_SILVER, '🎆'),
    ('day_children', '어린이날 출근', '5/5 출근 (자조)', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '05-05')), TIER_GOLD, '🎀'),
    ('day_hangul', '한글날 출근', '10/9 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '10-09')), TIER_SILVER, '🎤'),
    ('day_valentine', '발렌타인데이 출근', '2/14 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '02-14')), TIER_BRONZE, '💝'),
    ('day_white', '화이트데이 출근', '3/14 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '03-14')), TIER_BRONZE, '🌹'),
    ('day_pepero', '빼빼로데이', '11/11 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '11-11')), TIER_SILVER, '🍫'),
    ('day_halloween', '핼러윈 출근', '10/31 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '10-31')), TIER_BRONZE, '🎃'),
    ('day_aprilfools', '만우절 출근', '4/1 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '04-01')), TIER_BRONZE, '🃏'),
    ('day_77', '칠월칠석', '7/7 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '07-07')), TIER_SILVER, '🎋'),
    ('day_dongji', '동지 출근', '12/22 출근', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '12-22')), TIER_BRONZE, '🎇'),
    ('day_parents', '어버이날 정시 퇴근', '5/8 정시 퇴근', 1,
        _bool_eval(lambda db: _has_punctual_clockout_on(db, '05-08')),
        TIER_SILVER, '🪅'),
    ('day_teacher', '스승의 날 정시 퇴근', '5/15 정시 퇴근', 1,
        _bool_eval(lambda db: _has_punctual_clockout_on(db, '05-15')),
        TIER_BRONZE, '🎂'),
    ('day_xmas_eve', '크리스마스이브 정시 퇴근', '12/24 정시 퇴근', 1,
        _bool_eval(lambda db: _has_punctual_clockout_on(db, '12-24')),
        TIER_SILVER, '🎁'),
    ('day_earth', '지구의 날', '4/22 출근 (시크릿)', 1,
        _bool_eval(lambda db: _has_clockin_on(db, '04-22')), TIER_GOLD, '🌏'),
]


# ---- 11. 시즌·월별 ----
def _make_month_full_attendance_eval(month: int):
    """해당 월 영업일 모두 출근."""
    def _eval(db):
        year = date.today().year
        target = _workdays_in_year_month(year, month)
        cur = _count_in_year_month(db, year, month)
        # 영업일 수 정확히 카운트는 holidays 제외 안 함 — 단순화
        return min(cur, target), max(target, 1)
    return _eval


def _make_month_first_eval(month: int):
    def _eval(db):
        year = date.today().year
        return ((1, 1) if _count_in_year_month(db, year, month) >= 1 else (0, 1))
    return _eval


_SEASON_DEFS = [
    ('season_jan', '1월 정착', '1월 한 달 출근', 1,
        _make_month_first_eval(1), TIER_BRONZE, '⛄'),
    ('season_feb', '2월 정착', '2월 영업일 모두 출근', 1,
        _make_month_full_attendance_eval(2), TIER_SILVER, '🌨️'),
    ('season_mar', '봄을 맞이', '3월 첫 출근', 1,
        _make_month_first_eval(3), TIER_BRONZE, '🌸'),
    ('season_apr', '4월 정착', '4월 한 달 출근', 1,
        _make_month_full_attendance_eval(4), TIER_BRONZE, '🌷'),
    ('season_may', '5월 정착', '5월 영업일 모두 출근', 1,
        _make_month_full_attendance_eval(5), TIER_SILVER, '🌺'),
    ('season_jun', '여름의 시작', '6월 첫 출근', 1,
        _make_month_first_eval(6), TIER_BRONZE, '☀️'),
    ('season_jul', '7월 정착', '7월 한 달 출근', 1,
        _make_month_full_attendance_eval(7), TIER_BRONZE, '🌻'),
    ('season_aug', '8월 정착', '8월 영업일 모두 출근', 1,
        _make_month_full_attendance_eval(8), TIER_SILVER, '🍦'),
    ('season_sep', '가을의 시작', '9월 첫 출근', 1,
        _make_month_first_eval(9), TIER_BRONZE, '🍂'),
    ('season_oct', '10월 정착', '10월 한 달 출근', 1,
        _make_month_full_attendance_eval(10), TIER_BRONZE, '🌾'),
    ('season_nov', '11월 단풍', '11월 영업일 모두 출근', 1,
        _make_month_full_attendance_eval(11), TIER_SILVER, '🍁'),
    ('season_dec', '겨울의 시작', '12월 첫 출근', 1,
        _make_month_first_eval(12), TIER_BRONZE, '❄️'),
]


# ---- 12. 앱 사용 마일스톤 ----
_MILESTONE_DEFS = [
    ('mile_first', 'Hello, World!', '앱 첫 실행', 1,
        _bool_eval(lambda db: _count_work_records(db) >= 1 or _days_since_first_work(db) >= 0),
        TIER_BRONZE, '👋'),
    ('mile_7days', '일주일 사용', '7일 사용', 7,
        _make_count_eval(_days_since_first_work, 7), TIER_BRONZE, '🗓️'),
    ('mile_30days', '한 달 사용', '30일 사용', 30,
        _make_count_eval(_days_since_first_work, 30), TIER_SILVER, '📚'),
    ('mile_365days', '1주년', '365일 사용', 365,
        _make_count_eval(_days_since_first_work, 365), TIER_PLATINUM, '💎'),
    ('mile_730days', '2주년', '730일 사용', 730,
        _make_count_eval(_days_since_first_work, 730), TIER_LEGEND, '🌟'),
    ('mile_1095days', '3주년', '3년 사용', 1095,
        _make_count_eval(_days_since_first_work, 1095), TIER_LEGEND, '🎖️'),
    ('mile_5years', '5년 사용자', '5년 사용', 1825,
        _make_count_eval(_days_since_first_work, 1825), TIER_LEGEND, '🏆'),
    ('mile_10years', '10년 사용자', '10년 사용', 3650,
        _make_count_eval(_days_since_first_work, 3650), TIER_LEGEND, '🎖️'),
]


# ---- 13. 통계·분석 (view counter 기반) ----
_STATS_DEFS = [
    ('stat_weekly_10', '주간 통계러', '주간 탭 10회 조회', 10,
        _make_count_eval(lambda db: _setting_int(db, 'stat_weekly_view_count'), 10),
        TIER_BRONZE, '📊'),
    ('stat_monthly_10', '월간 통계러', '월간 탭 10회', 10,
        _make_count_eval(lambda db: _setting_int(db, 'stat_monthly_view_count'), 10),
        TIER_BRONZE, '📈'),
    ('stat_pattern_10', '패턴 분석가', '패턴 탭 10회', 10,
        _make_count_eval(lambda db: _setting_int(db, 'stat_pattern_view_count'), 10),
        TIER_SILVER, '🔍'),
    ('stat_calendar_30', '캘린더 챔프', '캘린더 30회 조회', 30,
        _make_count_eval(lambda db: _setting_int(db, 'calendar_view_count'), 30),
        TIER_SILVER, '📅'),
    ('stat_report_first', '일일 보고서 첫 생성', '일일 보고 1회', 1,
        _make_count_eval(lambda db: _setting_int(db, 'daily_report_count'), 1),
        TIER_BRONZE, '📋'),
    ('stat_report_30', '보고서 챔프', '일일 보고 30회', 30,
        _make_count_eval(lambda db: _setting_int(db, 'daily_report_count'), 30),
        TIER_SILVER, '📰'),
    ('stat_chart_hover', '차트 호버 발견', '차트 hover 첫 발견', 1,
        _bool_eval(lambda db: db.get_setting('chart_hover_discovered', 'false').lower() == 'true'),
        TIER_BRONZE, '🎨'),
    ('stat_achievements_open', '도전과제 박물관', '도전과제 뷰 50회', 50,
        _make_count_eval(lambda db: _setting_int(db, 'achievements_view_count'), 50),
        TIER_BRONZE, '🦄'),
]


# ---- 14. 시크릿 ----
def _has_clock_in_palindrome(db) -> bool:
    """출근 시각이 회문 (HH:MM에서 H1H2:M1M2가 회문)."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT clock_in FROM work_records WHERE clock_in IS NOT NULL")
        for (t,) in cur.fetchall():
            if not t:
                continue
            digits = t[:5].replace(':', '')
            if len(digits) == 4 and digits == digits[::-1]:
                return True
    return False


def _has_clock_in_jackpot(db) -> bool:
    """출근 시각 모든 자릿수 동일 (11:11, 22:22)."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT clock_in FROM work_records WHERE clock_in IS NOT NULL")
        for (t,) in cur.fetchall():
            if not t:
                continue
            digits = t[:5].replace(':', '')
            if len(digits) == 4 and len(set(digits)) == 1:
                return True
    return False


def _has_friday_13th_clockin(db) -> bool:
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT 1 FROM work_records
            WHERE strftime('%w', date) = '5'
            AND CAST(strftime('%d', date) AS INTEGER) = 13
            LIMIT 1
        """)
        return cur.fetchone() is not None


def _has_777(db) -> bool:
    """7월 7일 7시 7분 출근."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT 1 FROM work_records
            WHERE strftime('%m-%d', date) = '07-07'
            AND clock_in LIKE '07:07%'
            LIMIT 1
        """)
        return cur.fetchone() is not None


def _has_exact_8h(db) -> bool:
    """정확히 8시간 0분 0초 근무."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT 1 FROM work_records
            WHERE clock_in IS NOT NULL AND clock_out IS NOT NULL
            AND total_hours = 8.0
            LIMIT 1
        """)
        return cur.fetchone() is not None


def _has_pi_day(db) -> bool:
    """3/14 1:59 출근 (π = 3.14159)."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT 1 FROM work_records
            WHERE strftime('%m-%d', date) = '03-14'
            AND clock_in LIKE '01:59%'
            LIMIT 1
        """)
        return cur.fetchone() is not None


def _has_fibonacci_minute(db) -> bool:
    """출근 시각 분이 피보나치 (1,2,3,5,8,13,21,34,55)."""
    fibs = {1, 2, 3, 5, 8, 13, 21, 34, 55}
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT CAST(strftime('%M', clock_in) AS INTEGER) FROM work_records
            WHERE clock_in IS NOT NULL
        """)
        return any(r[0] in fibs for r in cur.fetchall())


def _has_double_six(db) -> bool:
    """6/6 18:06 출근."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT 1 FROM work_records
            WHERE strftime('%m-%d', date) = '06-06'
            AND clock_in LIKE '18:06%'
            LIMIT 1
        """)
        return cur.fetchone() is not None


def _has_500_anniv_clockin(db) -> bool:
    """가입 후 정확히 365일 후 출근."""
    hire = _setting_str(db, 'hire_date', '')
    if not hire:
        return False
    try:
        d = datetime.strptime(hire, '%Y-%m-%d').date()
        target = d + timedelta(days=365)
        return _has_clockin_on_date(db, target)
    except ValueError:
        return False


_SECRET_DEFS = [
    ('secret_palindrome', '회문 시각', '출근 시각이 회문', 1,
        _bool_eval(_has_clock_in_palindrome), TIER_GOLD, '🪞'),
    ('secret_jackpot', '잭팟 시각', '출근 시각 모든 자릿수 동일', 1,
        _bool_eval(_has_clock_in_jackpot), TIER_PLATINUM, '🎰'),
    ('secret_fri13', '13일 금요일', '13일 금요일 출근', 1,
        _bool_eval(_has_friday_13th_clockin), TIER_GOLD, '🌑'),
    ('secret_777', '7-7-7', '7월 7일 7시 7분 출근', 1,
        _bool_eval(_has_777), TIER_LEGEND, '🔮'),
    ('secret_exact_8h', '정확 8시간', '정확히 8h 0m 근무', 1,
        _bool_eval(_has_exact_8h), TIER_PLATINUM, '🎯'),
    ('secret_pi_day', '파이 데이', '3/14 01:59 출근', 1,
        _bool_eval(_has_pi_day), TIER_LEGEND, '🥧'),
    ('secret_fibonacci', '피보나치', '출근 분이 피보나치 수', 1,
        _bool_eval(_has_fibonacci_minute), TIER_SILVER, '🔢'),
    ('secret_double_six', '더블 식스', '6/6 18:06 출근', 1,
        _bool_eval(_has_double_six), TIER_LEGEND, '🎲'),
    ('secret_anniversary', '마법사', '가입 후 정확히 365일 후 출근', 1,
        _bool_eval(_has_500_anniv_clockin), TIER_LEGEND, '🧙'),
]


# ---- 15. 다양성·설정 ----
def _setting_changed_from_default(db, key: str, default_value: str) -> bool:
    return str(db.get_setting(key, default_value)) != default_value


_SETTINGS_DEFS = [
    ('set_dark', '다크 사이드', '다크 테마 1회 사용', 1,
        _bool_eval(lambda db: _setting_changed_from_default(db, 'theme', 'light')),
        TIER_BRONZE, '🌗'),
    ('set_lang', '이중언어', '언어 변경 (en 사용)', 1,
        _bool_eval(lambda db: db.get_setting('language', 'ko') == 'en'),
        TIER_BRONZE, '🌐'),
    ('set_a11y', '접근성 활용', '글꼴 크기≠100% 또는 고대비 ON', 1,
        _bool_eval(lambda db: db.get_setting('font_scale', '1.0') != '1.0'
                    or db.get_setting('high_contrast', 'false').lower() == 'true'),
        TIER_BRONZE, '♿'),
    ('set_overtime_unit', '단위 변경', 'overtime_unit 변경', 1,
        _bool_eval(lambda db: db.get_setting('overtime_unit', '30') != '30'),
        TIER_BRONZE, '⏱️'),
    ('set_goal_full', '목표 마스터', '월 연장+일평균 둘 다 설정', 1,
        _bool_eval(lambda db: _setting_int(db, 'goal_overtime_max_monthly') > 0
                    and float(db.get_setting('goal_avg_hours_daily', '0') or 0) > 0),
        TIER_SILVER, '🎯'),
    ('set_discord_full', '풀 셋업', 'Discord URL + 모든 알림 ON', 1,
        _bool_eval(lambda db: bool(db.get_setting('discord_webhook_url', '') or '')
                    and all(db.get_setting(k, 'true').lower() == 'true' for k in
                              ('notification_clock_out', 'notification_lunch',
                               'notification_overtime', 'notification_health'))),
        TIER_SILVER, '🔔'),
    ('set_cloud', '클라우드 동기화', 'DB 경로 변경', 1,
        _bool_eval(lambda db: bool(db.get_setting('db_path_override', '') or '')),
        TIER_SILVER, '☁️'),
]


# ---- 16. 메타 (도전과제 자체) ----
def _earned_count(db) -> int:
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("SELECT COUNT(*) FROM achievements WHERE earned_date IS NOT NULL")
        return cur.fetchone()[0]


def _earned_secret_count(db) -> int:
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT COUNT(*) FROM achievements
            WHERE earned_date IS NOT NULL AND is_secret = 1
        """)
        return cur.fetchone()[0]


_META_DEFS = [
    ('meta_first', '첫 도전과제', '첫 도전과제 획득', 1,
        _make_count_eval(_earned_count, 1), TIER_BRONZE, '🏆'),
    ('meta_10', '10개 달성', '10개 보유', 10,
        _make_count_eval(_earned_count, 10), TIER_BRONZE, '🎖️'),
    ('meta_25', '25개 달성', '25개 보유', 25,
        _make_count_eval(_earned_count, 25), TIER_SILVER, '🥈'),
    ('meta_50', '50개 달성', '50개 보유', 50,
        _make_count_eval(_earned_count, 50), TIER_GOLD, '🥇'),
    ('meta_75', '75개 달성', '75개 보유', 75,
        _make_count_eval(_earned_count, 75), TIER_PLATINUM, '💎'),
    ('meta_100', '100개 달성', '100개 보유', 100,
        _make_count_eval(_earned_count, 100), TIER_LEGEND, '🌟'),
    ('meta_secret_1', '시크릿 발견', '첫 시크릿 발견', 1,
        _make_count_eval(_earned_secret_count, 1), TIER_SILVER, '🔍'),
    ('meta_secret_5', '시크릿 헌터', '시크릿 5개 발견', 5,
        _make_count_eval(_earned_secret_count, 5), TIER_GOLD, '🌑'),
]


# ============================================================
# 모든 도전과제 통합
# ============================================================

def _build_all() -> List[Achievement]:
    """모든 카테고리를 합쳐 Achievement 리스트로 반환."""
    all_defs = []
    sections = [
        (CAT_STREAK, _STREAK_DEFS),
        (CAT_PUNCTUAL, _PUNCTUAL_DEFS),
        (CAT_BALANCE, _BALANCE_DEFS),
        (CAT_OT_BANK, _OT_BANK_DEFS),
        (CAT_OT_USE, _OT_USE_DEFS),
        (CAT_LEAVE, _LEAVE_DEFS),
        (CAT_MEAL, _MEAL_DEFS),
        (CAT_BREAK, _BREAK_DEFS),
        (CAT_TIME_SLOT, _TIME_SLOT_DEFS),
        (CAT_SPECIAL_DAY, _SPECIAL_DAY_DEFS),
        (CAT_SEASON, _SEASON_DEFS),
        (CAT_MILESTONE, _MILESTONE_DEFS),
        (CAT_STATS, _STATS_DEFS),
        (CAT_SETTINGS, _SETTINGS_DEFS),
        (CAT_SECRET, _SECRET_DEFS),
        (CAT_META, _META_DEFS),
    ]
    for cat, defs in sections:
        for tup in defs:
            code, name, desc, target, evaluator, tier, icon = tup
            is_secret = (cat == CAT_SECRET)
            all_defs.append(Achievement(
                code=code, name=name, description=desc, category=cat,
                tier=tier, badge_icon=icon, is_secret=is_secret,
                target=target, evaluator=evaluator,
            ))
    return all_defs


ALL_ACHIEVEMENTS: List[Achievement] = _build_all()


def get_achievement(code: str) -> Optional[Achievement]:
    for a in ALL_ACHIEVEMENTS:
        if a.code == code:
            return a
    return None


# ============================================================
# DB 동기화 + 평가
# ============================================================

def sync_definitions_to_db(db) -> None:
    """ALL_ACHIEVEMENTS 정의를 achievements 테이블에 upsert.

    code가 unique key. 신규 도전과제는 INSERT, 기존은 메타데이터 UPDATE
    (earned_date, progress는 보존).
    """
    with db._conn() as conn:
        cur = conn.cursor()
        for a in ALL_ACHIEVEMENTS:
            cur.execute("SELECT id FROM achievements WHERE code = ?", (a.code,))
            row = cur.fetchone()
            if row is None:
                cur.execute('''
                    INSERT INTO achievements
                    (code, name, description, category, tier, is_secret,
                     progress, target, badge_icon)
                    VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)
                ''', (a.code, a.name, a.description, a.category, a.tier,
                       1 if a.is_secret else 0, a.target, a.badge_icon))
            else:
                cur.execute('''
                    UPDATE achievements
                    SET name = ?, description = ?, category = ?, tier = ?,
                        is_secret = ?, target = ?, badge_icon = ?
                    WHERE code = ?
                ''', (a.name, a.description, a.category, a.tier,
                       1 if a.is_secret else 0, a.target, a.badge_icon, a.code))
        conn.commit()


def evaluate_all(db) -> List[Achievement]:
    """미획득 도전과제만 평가. 새로 잠금 해제된 것 리스트 반환.

    side effect: progress 업데이트, earned_date 기록.
    """
    newly_unlocked = []
    with db._conn() as conn:
        cur = conn.cursor()
        for a in ALL_ACHIEVEMENTS:
            if a.evaluator is None:
                continue
            cur.execute("""
                SELECT progress, earned_date FROM achievements WHERE code = ?
            """, (a.code,))
            row = cur.fetchone()
            if row is None:
                continue
            stored_progress, earned = row[0], row[1]
            if earned is not None:
                continue  # 이미 획득

            try:
                progress, target = a.evaluator(db)
            except Exception:
                continue  # 평가 실패는 silent (다음 tick에 재시도)

            now_unlocked = progress >= target

            if progress != stored_progress or now_unlocked:
                if now_unlocked:
                    cur.execute("""
                        UPDATE achievements
                        SET progress = ?, earned_date = DATE('now', 'localtime')
                        WHERE code = ?
                    """, (target, a.code))
                    newly_unlocked.append(a)
                else:
                    cur.execute("""
                        UPDATE achievements SET progress = ? WHERE code = ?
                    """, (progress, a.code))
        conn.commit()
    return newly_unlocked


def get_all_with_status(db) -> List[dict]:
    """UI용: 모든 도전과제 + 진행도/획득 상태 dict 리스트."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT code, name, description, category, tier, is_secret,
                   progress, target, earned_date, badge_icon
            FROM achievements
            ORDER BY
                CASE WHEN earned_date IS NOT NULL THEN 0 ELSE 1 END,
                category, tier
        """)
        return [dict(zip(
            ['code', 'name', 'description', 'category', 'tier', 'is_secret',
             'progress', 'target', 'earned_date', 'badge_icon'],
            row
        )) for row in cur.fetchall()]


def get_stats(db) -> dict:
    """전체 통계 — 획득/총개수/비밀발견 등."""
    with db._conn() as conn:
        cur = conn.cursor()
        cur.execute("""
            SELECT
                COUNT(*) AS total,
                SUM(CASE WHEN earned_date IS NOT NULL THEN 1 ELSE 0 END) AS earned,
                SUM(CASE WHEN is_secret = 1 THEN 1 ELSE 0 END) AS secret_total,
                SUM(CASE WHEN is_secret = 1 AND earned_date IS NOT NULL THEN 1 ELSE 0 END) AS secret_earned
            FROM achievements
        """)
        row = cur.fetchone()
        return {
            'total': row[0] or 0,
            'earned': row[1] or 0,
            'secret_total': row[2] or 0,
            'secret_earned': row[3] or 0,
        }
