#!/usr/bin/env python3
"""
스토커 데이터 통합 추출 스크립트 v2

모든 JSON 소스에서 데이터를 추출하여 중간 데이터 파일 생성
- DT_Skill: 스킬 상세 정보
- DT_CharacterStat/Ability: 스토커 기본 정보
- Blueprint: 스킬 변수 (ActivationOrderGroup 등)
- AnimMontage: 평타/스킬 타이밍, 공격 노티파이
"""

import json
import sys
import re
from pathlib import Path
from typing import Dict, List, Any, Optional

# config 임포트
sys.path.append(str(Path(__file__).parent))
import config

def format_description(desc: str, desc_values: List) -> str:
    """
    desc 문자열의 {0}, {1}, {2} 등을 descValues 배열 값으로 치환

    Args:
        desc: 원본 설명 문자열 (예: "방패를 들어 {0}초 동안 반격 자세를 취합니다. 반격 성공 시 {1}%만큼 물리 피해를 줍니다.")
        desc_values: 값 배열 (예: [5, 80])

    Returns:
        치환된 설명 문자열 (예: "방패를 들어 5초 동안 반격 자세를 취합니다. 반격 성공 시 80%만큼 물리 피해를 줍니다.")
    """
    if not desc or not desc_values:
        return desc

    # {0}, {1}, {2} 등을 descValues로 치환
    result = desc
    for i, value in enumerate(desc_values):
        placeholder = f"{{{i}}}"
        result = result.replace(placeholder, str(value))

    # 줄바꿈 제거 (마크다운 호환성)
    result = result.replace('\n', ' ').replace('\r', ' ')

    return result

def load_json(file_path: Path) -> Dict:
    """JSON 파일 로드"""
    print(f"Loading: {file_path.name}")
    with open(file_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    return data

def find_table(datatables: List[Dict], table_name: str) -> Optional[Dict]:
    """DataTable.json에서 특정 테이블 찾기"""
    for dt in datatables:
        if dt.get('AssetName') == table_name:
            return dt
    return None

def find_asset_by_name(assets: List[Dict], name_pattern: str) -> List[Dict]:
    """에셋 이름 패턴으로 검색"""
    return [a for a in assets if name_pattern in a.get('AssetName', '')]

def extract_character_stats(datatables: List[Dict]) -> Dict[str, Dict]:
    """
    DT_CharacterStat에서 스토커 기본 정보 추출

    Returns:
        {stalker_id: {name, job, stats, skills, ...}}
    """
    print("\n=== DT_CharacterStat 추출 ===")
    char_stat_table = find_table(datatables, 'DT_CharacterStat')
    if not char_stat_table:
        print("[WARN] DT_CharacterStat 테이블을 찾을 수 없습니다.")
        return {}

    stalker_data = {}
    for row in char_stat_table.get('Rows', []):
        row_name = row['RowName']
        if row_name not in config.STALKERS:
            continue

        data = row['Data']

        # 스토커 이름 포맷: "English (Korean)"
        korean_name = data.get('name', '')
        info = config.STALKER_INFO.get(row_name, {})
        english_name = info.get('english', row_name.capitalize())
        formatted_name = f"{english_name} ({korean_name})" if korean_name else english_name

        stalker_data[row_name] = {
            'id': row_name,
            'name': formatted_name,  # 영문(한글) 형식
            'koreanName': korean_name,  # 순수 한글 이름
            'englishName': english_name,  # 순수 영문 이름
            'jobName': data.get('jobName', ''),
            'stats': {
                'str': data.get('str', 0),
                'dex': data.get('dex', 0),
                'int': data.get('int', 0),
                'con': data.get('con', 0),
                'wis': data.get('wis', 0)
            },
            'hp': data.get('hP', 0),
            'mp': data.get('mP', 0),
            'manaRegen': round(data.get('manaRegen', 0), 2),  # 소수점 2자리
            'physicalDamage': data.get('physicalDamage', 0),
            'magicalDamage': data.get('magicalDamage', 0),
            'criticalPer': data.get('criticalPer', 5),  # 크리티컬 확률
            'criticalDamage': data.get('criticalDamage', 0),  # 크리티컬 추가 피해
            'defaultSkills': data.get('defaultSkills', []),
            'subSkill': data.get('subSkill', ''),
            'ultimateSkill': data.get('ultimateSkill', ''),
            'equipableTypes': data.get('equipableTypes', []),
            'ultimatePoint': data.get('ultimatePoint', 0),
            'source': 'DT_CharacterStat'
        }
        print(f"  [OK] {stalker_data[row_name]['name']} ({row_name})")

    return stalker_data

def extract_character_abilities(datatables: List[Dict]) -> Dict[str, Dict]:
    """
    DT_CharacterAbility에서 평타 몽타주 정보 추출

    Returns:
        {stalker_id: {attackMontageMap, abilities}}
    """
    print("\n=== DT_CharacterAbility 추출 ===")
    char_ability_table = find_table(datatables, 'DT_CharacterAbility')
    if not char_ability_table:
        print("⚠️  DT_CharacterAbility 테이블을 찾을 수 없습니다.")
        return {}

    stalker_abilities = {}
    for row in char_ability_table.get('Rows', []):
        row_name = row['RowName']
        if row_name not in config.STALKERS:
            continue

        data = row['Data']
        stalker_abilities[row_name] = {
            'attackMontageMap': data.get('attackMontageMap', {}),
            'abilities': data.get('abilities', []),
            'source': 'DT_CharacterAbility'
        }

        # 평타 콤보 수 계산
        combo_counts = {}
        for weapon_type, montage_data in stalker_abilities[row_name]['attackMontageMap'].items():
            montage_array = montage_data.get('montageArray', [])
            combo_counts[weapon_type] = len(montage_array)

        print(f"  [OK] {row_name}: {combo_counts}")

    return stalker_abilities

def extract_skills(datatables: List[Dict]) -> Dict[str, Dict]:
    """
    DT_Skill에서 모든 스킬 정보 추출

    Returns:
        {skill_id: {skill data}}
    """
    print("\n=== DT_Skill 추출 ===")
    skill_table = find_table(datatables, 'DT_Skill')
    if not skill_table:
        print("⚠️  DT_Skill 테이블을 찾을 수 없습니다.")
        return {}

    all_skills = {}
    for row in skill_table.get('Rows', []):
        skill_id = row['RowName']
        data = row['Data']

        # 스토커 스킬만 추출
        stalker_name = data.get('stalkerName', '')
        if stalker_name not in config.STALKERS:
            continue

        # 설명 처리: desc에서 {0}, {1} 등을 descValues로 치환
        desc_raw = data.get('desc', '')
        desc_values_raw = data.get('descValues', [])

        # descValues의 float 값을 소수점 둘째자리로 반올림
        desc_values = []
        for val in desc_values_raw:
            if isinstance(val, float):
                desc_values.append(round(val, 2))
            else:
                desc_values.append(val)

        desc_formatted = format_description(desc_raw, desc_values)

        all_skills[skill_id] = {
            'skillId': skill_id,
            'stalkerName': stalker_name,
            'name': data.get('name', ''),
            'desc': desc_raw,  # 원본 desc (변수 포함)
            'descFormatted': desc_formatted,  # 변수 치환된 desc
            'descValues': desc_values,  # descValues 배열
            'simpleDesc': data.get('simpleDesc', ''),
            'bIsUltimate': data.get('bIsUltimate', False),
            'bIsStackable': data.get('bIsStackable', False),
            'maxStackCount': data.get('maxStackCount', 0),
            'skillDamageRate': data.get('skillDamageRate', 0),
            'skillAttackType': data.get('skillAttackType', ''),
            'skillElementType': data.get('skillElementType', ''),
            'manaCost': data.get('manaCost', 0),
            'coolTime': data.get('coolTime', 0),
            'castingTime': data.get('castingTime', 0),
            'activeDuration': data.get('activeDuration', 0),  # 소환수 지속시간
            'activeRange': data.get('activeRange', {}),  # tick, count, dist 등
            'useMontages': data.get('useMontages', []),
            'gameplayEffectSet': data.get('gameplayEffectSet', []),
            'abilityClass': data.get('abilityClass', ''),
            'icon': data.get('icon', ''),
            'bUsable': data.get('bUsable', False),
            'bUnSelectable': data.get('bUnSelectable', False),
            'source': 'DT_Skill'
        }

    print(f"  [OK] 총 {len(all_skills)}개 스킬 추출")

    # 스토커별 카운트
    stalker_counts = {}
    for skill in all_skills.values():
        stalker = skill['stalkerName']
        stalker_counts[stalker] = stalker_counts.get(stalker, 0) + 1

    for stalker_id in config.STALKERS:
        count = stalker_counts.get(stalker_id, 0)
        print(f"    - {stalker_id}: {count}개")

    return all_skills

def extract_skill_blueprints(blueprints: List[Dict]) -> Dict[str, Dict]:
    """
    Blueprint.json에서 GA_Skill_ 블루프린트의 변수 추출

    Returns:
        {blueprint_name: {variables}}
    """
    print("\n=== GA_Skill Blueprint 추출 ===")

    skill_blueprints = {}
    ga_skills = [bp for bp in blueprints if 'GA_Skill' in bp.get('AssetName', '')]

    for bp in ga_skills:
        asset_name = bp['AssetName']
        variables = {}

        for var in bp.get('Variables', []):
            var_name = var.get('Name', '')
            variables[var_name] = {
                'name': var_name,
                'type': var.get('Type', var.get('Category', 'unknown')),
                'defaultValue': var.get('DefaultValue', 'N/A'),
                'source': var.get('Source', 'Blueprint'),
                'category': var.get('CategoryName', ''),
                'isEditable': var.get('IsEditable', False),
                'isBlueprintVisible': var.get('IsBlueprintVisible', False)
            }

        skill_blueprints[asset_name] = {
            'assetName': asset_name,
            'assetPath': bp.get('AssetPath', ''),
            'parentClass': bp.get('ParentClass', ''),
            'variables': variables,
            'source': 'Blueprint'
        }

    print(f"  [OK] 총 {len(skill_blueprints)}개 GA_Skill Blueprint 추출")
    return skill_blueprints

def extract_anim_montages(montages: List[Dict]) -> Dict[str, Dict]:
    """
    AnimMontage.json에서 몽타주 타이밍 및 노티파이 추출
    - AddNormalAttackPer 추출 (ANS_AttackState_C 노티파이)
    - attackStateEndTime 추출 (ANS_AttackState_C 종료 시점)

    Returns:
        {montage_name: {timing, notifies, attackMultiplier, attackStateEndTime}}
    """
    print("\n=== AnimMontage 추출 ===")

    all_montages = {}
    pc_montages = [m for m in montages if 'AM_PC_' in m.get('AssetName', '') or 'AM_Sum_' in m.get('AssetName', '')]

    for montage in pc_montages:
        asset_name = montage['AssetName']

        # 공격 노티파이 추출
        attack_notifies = []
        attack_multiplier = 0.0  # AddNormalAttackPer (기본값 0)
        attack_state_end_time = None  # ANS_AttackState 종료 시점 (평타용)

        for notify in montage.get('AnimNotifies', []):
            notify_class = notify.get('NotifyClass', '')
            notify_state_class = notify.get('NotifyStateClass', '')
            notify_name = notify.get('NotifyName', '')
            custom_props = notify.get('CustomProperties', {})

            # ANS_AttackState_C에서 AddNormalAttackPer 및 종료 시점 추출
            if 'ANS_AttackState' in notify_state_class:
                add_normal_attack_str = custom_props.get('AddNormalAttackPer', '0')
                try:
                    attack_multiplier = float(add_normal_attack_str)
                except (ValueError, TypeError):
                    attack_multiplier = 0.0

                # ANS_AttackState 종료 시점 = TriggerTime + Duration
                trigger_time = notify.get('TriggerTime', 0)
                duration = notify.get('Duration', 0)
                attack_state_end_time = trigger_time + duration

            # 공격 판정 로직 (우선순위)
            is_attack_notify = False

            # 1. NotifyName에 키워드 포함 (부분 매칭)
            if any(keyword in notify_name for keyword in config.ATTACK_NOTIFY_KEYWORDS):
                is_attack_notify = True

            # 2. CustomProperties의 NotifyName 확인 - 1순위 실패 시
            if not is_attack_notify:
                custom_notify_name = custom_props.get('NotifyName', '')
                if custom_notify_name and any(keyword in custom_notify_name for keyword in config.ATTACK_NOTIFY_KEYWORDS):
                    is_attack_notify = True

            # 3. NotifyClass에 키워드 포함 (부분 매칭) - 1, 2순위 실패 시
            if not is_attack_notify:
                if any(keyword in notify_class for keyword in config.ATTACK_NOTIFY_KEYWORDS):
                    is_attack_notify = True

            # 4. SimpleSendEvent의 Event Tag 확인 (1, 2, 3순위 실패 시)
            if not is_attack_notify and 'SimpleSendEvent' in notify_class:
                event_tag = custom_props.get('Event Tag', '')
                if any(attack_tag in event_tag for attack_tag in config.ATTACK_EVENT_TAGS):
                    is_attack_notify = True

            if is_attack_notify:
                attack_notifies.append({
                    'notifyName': notify_name,
                    'notifyClass': notify_class,
                    'notifyStateClass': notify_state_class,
                    'triggerTime': notify.get('TriggerTime', 0),
                    'duration': notify.get('Duration', 0),
                    'notifyType': notify.get('NotifyType', ''),
                    'customProperties': custom_props
                })

        # 시퀀스 길이 = SequenceLength / RateScale (actualDuration)
        seq_len = montage.get('SequenceLength', 0)
        rate_scale = montage.get('RateScale', 1.0)
        actual_duration = seq_len / rate_scale if rate_scale > 0 else seq_len

        all_montages[asset_name] = {
            'assetName': asset_name,
            'assetPath': montage.get('AssetPath', ''),
            'sequenceLength': seq_len,
            'rateScale': rate_scale,
            'actualDuration': actual_duration,  # 시퀀스 길이 (SequenceLength / RateScale)
            'attackStateEndTime': attack_state_end_time,  # ANS_AttackState 종료 시점 (평타용)
            'attackMultiplier': attack_multiplier,  # AddNormalAttackPer
            'sections': montage.get('Sections', []),
            'numSections': montage.get('NumSections', 0),
            'allNotifies': montage.get('AnimNotifies', []),
            'attackNotifies': attack_notifies,
            'hasAttack': len(attack_notifies) > 0,
            'blendInTime': montage.get('BlendInTime', 0),
            'blendOutTime': montage.get('BlendOutTime', 0),
            'source': 'AnimMontage'
        }

    print(f"  [OK] 총 {len(all_montages)}개 몽타주 추출 (PC + Summon)")

    # 소환수 몽타주 확인
    summon_montages = [m for m in all_montages.keys() if 'Summon' in m or 'Sum_' in m]
    if summon_montages:
        print(f"  [INFO] 소환수 관련 몽타주: {len(summon_montages)}개")
        for sm in summon_montages:
            seq_len = all_montages[sm]['sequenceLength']
            actual_dur = all_montages[sm]['actualDuration']
            has_attack = all_montages[sm]['hasAttack']
            print(f"    - {sm}: {seq_len:.2f}초 (실제: {actual_dur:.2f}초), 공격={has_attack}")

    return all_montages

def extract_npc_abilities(datatables: List[Dict]) -> Dict[str, Dict]:
    """
    DT_NPCAbility에서 소환수(Ifrit, Shiva) 정보 추출

    Returns:
        {npc_name: {attackMontageMap}}
    """
    print("\n=== DT_NPCAbility 추출 ===")
    npc_ability_table = find_table(datatables, 'DT_NPCAbility')
    if not npc_ability_table:
        print("[WARN] DT_NPCAbility 테이블을 찾을 수 없습니다.")
        return {}

    npc_abilities = {}
    summon_names = ['ifrit', 'shiva']  # Rene의 소환수

    for row in npc_ability_table.get('Rows', []):
        row_name = row['RowName'].lower()
        if row_name not in summon_names:
            continue

        data = row['Data']
        attack_map = data.get('attackMontageMap', {})

        npc_abilities[row_name] = {
            'npcName': row['RowName'],
            'attackMontageMap': attack_map,
            'source': 'DT_NPCAbility'
        }

        # 몽타주 개수 출력
        for weapon_type, montage_data in attack_map.items():
            montage_array = montage_data.get('montageArray', [])
            print(f"  [OK] {row['RowName']} ({weapon_type}): {len(montage_array)}개 몽타주")
            for i, montage_path in enumerate(montage_array):
                montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
                print(f"       {i+1}. {montage_name}")

    return npc_abilities

def extract_runes(datatables: List[Dict]) -> Dict[str, Dict]:
    """
    DT_Rune에서 룬 데이터 추출

    Returns:
        {runeId: {runeSet, level, name, desc, attributeModifies, ...}}
    """
    print("\n=== DT_Rune 추출 ===")
    rune_table = find_table(datatables, 'DT_Rune')
    if not rune_table:
        print("[WARN] DT_Rune 테이블을 찾을 수 없습니다.")
        return {}

    runes = {}
    for row in rune_table.get('Rows', []):
        rune_id = row['RowName']
        data = row['Data']

        # attributeModifies 파싱
        attr_modifies = []
        for mod in data.get('attributeModifies', []):
            attr = mod.get('attribute', {})
            attr_modifies.append({
                'attributeName': attr.get('attributeName', ''),
                'value': mod.get('value', 0)
            })

        runes[rune_id] = {
            'runeId': rune_id,
            'runeSet': data.get('runeSet', ''),
            'level': data.get('level', 1),
            'name': data.get('runeName', ''),
            'desc': format_description(data.get('desc', ''), data.get('descValue', [])),
            'descValue': data.get('descValue', []),
            'attributeModifies': attr_modifies,
            'unlockGold': data.get('unlockGold', 0),
            'unlockSkillPoint': data.get('unlockSkillPoint', 0)
        }

    print(f"  [OK] {len(runes)}개 룬 데이터 추출 완료")
    return runes

def extract_rune_groups(datatables: List[Dict]) -> Dict[str, Dict]:
    """
    DT_RuneGroup에서 룬 그룹 데이터 추출

    Returns:
        {groupId: {name, type, coreLine, sub1Line, sub2Line}}
    """
    print("\n=== DT_RuneGroup 추출 ===")
    rune_group_table = find_table(datatables, 'DT_RuneGroup')
    if not rune_group_table:
        print("[WARN] DT_RuneGroup 테이블을 찾을 수 없습니다.")
        return {}

    groups = {}
    for row in rune_group_table.get('Rows', []):
        group_id = row['RowName']
        data = row['Data']

        groups[group_id] = {
            'groupId': group_id,
            'name': data.get('name', ''),
            'type': data.get('type', ''),
            'coreLine': data.get('coreLine', []),
            'sub1Line': data.get('sub1Line', []),
            'sub2Line': data.get('sub2Line', [])
        }

        print(f"  [OK] {data.get('name', group_id)}: Core({len(data.get('coreLine', []))}), Sub1({len(data.get('sub1Line', []))}), Sub2({len(data.get('sub2Line', []))})")

    return groups

def extract_equipment(datatables: List[Dict]) -> Dict[str, Dict]:
    """
    DT_Equip에서 장비 데이터 추출

    Returns:
        {equipId: {name, equipSlotType, equipType, rarity, stats, ...}}
    """
    print("\n=== DT_Equip 추출 ===")
    equip_table = find_table(datatables, 'DT_Equip')
    if not equip_table:
        print("[WARN] DT_Equip 테이블을 찾을 수 없습니다.")
        return {}

    equipment = {}
    for row in equip_table.get('Rows', []):
        equip_id = row['RowName']
        data = row['Data']

        # stats 파싱
        stats = []
        for stat in data.get('stats', []):
            attr = stat.get('attribute', {})
            stats.append({
                'attributeName': attr.get('attributeName', ''),
                'value': stat.get('value', 0),
                'visible': stat.get('visible', False)
            })

        equipment[equip_id] = {
            'equipId': equip_id,
            'name': data.get('name', ''),
            'desc': data.get('desc', ''),
            'equipSlotType': data.get('equipSlotType', ''),
            'equipType': data.get('equipType', ''),
            'rarity': data.get('rarity', ''),
            'price': data.get('price', 0),
            'sellPrice': data.get('sellPrice', 0),
            'stats': stats,
            'armor': data.get('armor', 0)
        }

    print(f"  [OK] {len(equipment)}개 장비 데이터 추출 완료")
    return equipment

def extract_float_constants(datatables: List[Dict]) -> Dict[str, float]:
    """
    DT_Float에서 기어스코어 공식 상수 추출

    Returns:
        {constantName: value}
    """
    print("\n=== DT_Float (기어스코어 상수) 추출 ===")
    float_table = find_table(datatables, 'DT_Float')
    if not float_table:
        print("[WARN] DT_Float 테이블을 찾을 수 없습니다.")
        return {}

    constants = {}
    gearscore_keys = [
        'GearScoreEquipCommon',
        'GearScoreEquipUncommon',
        'GearScoreEquipRare',
        'GearScoreEquipLegendary',
        'GearScoreSkillPassive',
        'GearScoreSkillPerk'
    ]

    for row in float_table.get('Rows', []):
        row_name = row['RowName']
        if row_name in gearscore_keys:
            constants[row_name] = row['Data'].get('value', 0)
            print(f"  [OK] {row_name}: {constants[row_name]}")

    return constants

def organize_stalker_data(
    stalker_stats: Dict,
    stalker_abilities: Dict,
    all_skills: Dict,
    skill_blueprints: Dict,
    anim_montages: Dict,
    npc_abilities: Dict,
    runes: Dict,
    rune_groups: Dict,
    equipment: Dict,
    float_constants: Dict
) -> Dict[str, Dict]:
    """
    스토커별로 모든 데이터를 통합 정리

    Returns:
        {stalker_id: {모든 데이터}}
    """
    print("\n=== 스토커별 데이터 통합 ===")

    organized = {}

    for stalker_id in config.STALKERS:
        if stalker_id not in stalker_stats:
            print(f"  [WARN] {stalker_id}: 기본 스탯 없음")
            continue

        stats = stalker_stats[stalker_id]
        abilities = stalker_abilities.get(stalker_id, {})

        # 스토커의 스킬 목록
        skill_ids = stats['defaultSkills'] + [stats['subSkill'], stats['ultimateSkill']]
        skill_ids = [sid for sid in skill_ids if sid]  # 빈 문자열 제거

        # 스킬 상세 정보
        skills = {}
        for skill_id in skill_ids:
            if skill_id not in all_skills:
                print(f"    [WARN] {stalker_id}: 스킬 {skill_id} 정보 없음")
                continue

            skill_data = all_skills[skill_id].copy()

            # Blueprint 정보 매칭
            ability_class = skill_data.get('abilityClass', '')
            if ability_class:
                # '/Game/Blueprints/Abilities/GA_Skill_XXX.GA_Skill_XXX_C' -> 'GA_Skill_XXX'
                bp_name = ability_class.split('/')[-1].split('.')[0]
                if bp_name in skill_blueprints:
                    skill_data['blueprintVariables'] = skill_blueprints[bp_name]['variables']
                else:
                    skill_data['blueprintVariables'] = {}

            # AnimMontage 정보 매칭
            use_montages = skill_data.get('useMontages', [])
            skill_data['montageData'] = []
            for montage_path in use_montages:
                # '/Script/Engine.AnimMontage'/Game/_Art/.../ AM_XXX.AM_XXX' -> 'AM_XXX'
                montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
                if montage_name in anim_montages:
                    skill_data['montageData'].append(anim_montages[montage_name])
                else:
                    print(f"    [WARN] {skill_id}: 몽타주 {montage_name} 정보 없음")

            # 바란 궁극기 특수 처리: AN_SimpleSendEvent 시점을 castingTime으로 사용
            if skill_id == 'SK130301':  # 바란 궁극기 '일격분쇄'
                for montage_data in skill_data['montageData']:
                    for notify in montage_data.get('allNotifies', []):
                        if 'SimpleSendEvent' in notify.get('NotifyClass', ''):
                            event_tag = notify.get('CustomProperties', {}).get('Event Tag', '')
                            if 'Ability.Attack.Ready' in event_tag:
                                trigger_time = notify.get('TriggerTime', 0)
                                skill_data['castingTime'] = round(trigger_time, 2)
                                print(f"    [INFO] {skill_id}: castingTime 오버라이드 {skill_data['castingTime']}초 (AN_SimpleSendEvent)")
                                break

            # DoT 스킬 체크
            skill_data['isDot'] = skill_id in config.DOT_SKILLS

            # 소환수 스킬 체크 (유틸리티 판별보다 먼저 설정)
            skill_data['isSummon'] = skill_id in config.SUMMON_SKILLS
            if skill_data['isSummon']:
                summon_info = config.SUMMON_SKILLS.get(skill_id, {})
                summon_type = summon_info.get('type', 'npc')

                if summon_type == 'special':
                    # Shiva 특수 처리: 직접 몽타주 지정
                    skill_data['summonMontageData'] = []
                    montage_name = summon_info.get('montage')
                    attack_interval_bonus = summon_info.get('attack_interval_bonus', 0)

                    if montage_name and montage_name in anim_montages:
                        montage_info = anim_montages[montage_name].copy()
                        # 공격 주기 = 실제 시간 + 보너스
                        original_duration = montage_info['actualDuration']
                        montage_info['attackInterval'] = original_duration + attack_interval_bonus
                        skill_data['summonMontageData'].append(montage_info)
                        skill_data['summonType'] = 'special'
                else:
                    # Ifrit 등: DT_NPCAbility에서 추출
                    summon_name = summon_info.get('summon', '').lower()
                    if summon_name in npc_abilities:
                        npc_data = npc_abilities[summon_name]
                        attack_map = npc_data.get('attackMontageMap', {})
                        skill_data['summonAttackMap'] = attack_map

                        # 소환수 몽타주 데이터 추가
                        skill_data['summonMontageData'] = []
                        for weapon_type, montage_data in attack_map.items():
                            montage_array = montage_data.get('montageArray', [])
                            for montage_path in montage_array:
                                montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
                                if montage_name in anim_montages:
                                    skill_data['summonMontageData'].append(anim_montages[montage_name])
                        skill_data['summonType'] = 'npc'

            # 유틸리티 스킬 판별 (isSummon 설정 이후에 실행)
            skill_data['isUtility'] = is_utility_skill(skill_data)

            skills[skill_id] = skill_data

        # 평타 몽타주 상세 정보 매칭
        attack_montage_map = abilities.get('attackMontageMap', {})
        basic_attacks = {}
        for weapon_type, montage_data in attack_montage_map.items():
            montage_array = montage_data.get('montageArray', [])
            basic_attacks[weapon_type] = []
            for idx, montage_path in enumerate(montage_array):
                montage_name = montage_path.split('/')[-1].replace("'", "").split('.')[0]
                if montage_name in anim_montages:
                    montage_info = anim_montages[montage_name]

                    # 평타는 ANS_AttackState 종료 시점을 우선 사용
                    # 없으면 actualDuration 폴백
                    attack_state_end = montage_info.get('attackStateEndTime')
                    effective_duration = attack_state_end if attack_state_end is not None else montage_info['actualDuration']

                    basic_attacks[weapon_type].append({
                        'index': idx + 1,
                        'montageName': montage_name,
                        'sequenceLength': montage_info['sequenceLength'],
                        'rateScale': montage_info['rateScale'],
                        'actualDuration': montage_info['actualDuration'],  # 원본 몽타주 시간
                        'attackStateEndTime': attack_state_end,  # ANS_AttackState 종료 시점
                        'effectiveDuration': effective_duration,  # 실제 평타 시간 (ANS_AttackState 우선)
                        'attackMultiplier': montage_info['attackMultiplier'],
                        'hasAttack': montage_info['hasAttack']
                    })

        # 소환체 데이터 생성 (레네만)
        summons = {}
        for skill_id, skill_data in skills.items():
            if skill_data.get('isSummon'):
                summon_config = config.SUMMON_SKILLS.get(skill_id, {})
                summon_name = summon_config.get('summon', 'Unknown')

                # 공격 몽타주 정보 추출
                attack_montages = []
                for montage_data in skill_data.get('summonMontageData', []):
                    attack_montages.append({
                        'montageName': montage_data.get('assetName', 'N/A'),
                        'actualDuration': montage_data.get('actualDuration', 0)
                    })

                summons[summon_name] = {
                    'summonSkillId': skill_id,
                    'summonSkillName': skill_data.get('name', ''),
                    'activeDuration': skill_data.get('activeDuration', 0),
                    'skillDamageRate': skill_data.get('skillDamageRate', 0),  # 피해 배율 추가
                    'attackMontages': attack_montages,
                    'dotType': config.DOT_SKILLS.get(skill_id, {}).get('dot_type', '')
                }

        organized[stalker_id] = {
            'id': stalker_id,
            'stats': stats,
            'abilities': abilities,
            'basicAttacks': basic_attacks,  # 평타 상세 정보
            'skills': skills,
            'defaultSkills': [skills.get(sid) for sid in stats['defaultSkills'] if sid in skills],
            'subSkill': skills.get(stats['subSkill']),
            'ultimateSkill': skills.get(stats['ultimateSkill']),
            'summons': summons  # 소환체 정보
        }

        skill_count = len(skills)
        print(f"  [OK] {stats['name']} ({stalker_id}): {skill_count}개 스킬")

    # 공통 데이터 추가
    organized['_metadata'] = {
        'runes': runes,
        'runeGroups': rune_groups,
        'equipment': equipment,
        'gearScoreConstants': float_constants
    }

    return organized

def is_utility_skill(skill_data: Dict) -> bool:
    """
    유틸리티 스킬 판별 (DPS 계산 제외 대상)

    판별 기준:
    1. config.UTILITY_SKILLS에 명시적으로 등록
    2. skillAttackType == "Normal" AND skillDamageRate == 0
    3. 몽타주에 공격 노티파이 없음 (montageData 확인)

    예외: 소환 스킬은 항상 공격 스킬로 간주
    """
    skill_id = skill_data['skillId']

    # 소환 스킬은 공격 스킬 (유틸리티 아님)
    if skill_data.get('isSummon', False):
        return False

    # 1. 수동 지정
    if skill_id in config.UTILITY_SKILLS:
        return True

    # 2. Normal 타입 + Rate 0
    if skill_data['skillAttackType'] == 'Normal' and skill_data['skillDamageRate'] == 0:
        return True

    # 3. 몽타주에 공격 노티파이 없음
    montage_data_list = skill_data.get('montageData', [])
    if montage_data_list:
        has_attack = any(m.get('hasAttack', False) for m in montage_data_list)
        if not has_attack and skill_data['skillDamageRate'] > 0:
            # Rate는 있지만 공격 노티파이 없음 -> 유틸리티
            return True

    return False

def main():
    """메인 실행 함수"""
    print("="*80)
    print("스토커 데이터 통합 추출 v2")
    print("="*80)

    # 1. JSON 파일 로드
    print("\n[ JSON 파일 로드 ]")
    datatable_data = load_json(config.DATATABLE_JSON)
    blueprint_data = load_json(config.BLUEPRINT_JSON)
    animmontage_data = load_json(config.ANIMMONTAGE_JSON)

    datatables = datatable_data.get('Assets', [])
    blueprints = blueprint_data.get('Assets', [])
    montages = animmontage_data.get('Assets', [])

    # 2. 데이터 추출
    stalker_stats = extract_character_stats(datatables)
    stalker_abilities = extract_character_abilities(datatables)
    all_skills = extract_skills(datatables)
    skill_blueprints = extract_skill_blueprints(blueprints)
    anim_montages = extract_anim_montages(montages)
    npc_abilities = extract_npc_abilities(datatables)  # 소환수 데이터
    runes = extract_runes(datatables)  # 룬 데이터
    rune_groups = extract_rune_groups(datatables)  # 룬 그룹 데이터
    equipment = extract_equipment(datatables)  # 장비 데이터
    float_constants = extract_float_constants(datatables)  # 기어스코어 상수

    # 3. 데이터 통합
    organized_data = organize_stalker_data(
        stalker_stats,
        stalker_abilities,
        all_skills,
        skill_blueprints,
        anim_montages,
        npc_abilities,
        runes,
        rune_groups,
        equipment,
        float_constants
    )

    # 4. 결과 저장 (새 디렉토리 생성)
    output_dir = config.get_output_dir(create_new=True)
    output_dir.mkdir(parents=True, exist_ok=True)
    output_file = output_dir / "intermediate_data.json"

    with open(output_file, 'w', encoding='utf-8') as f:
        json.dump(organized_data, f, ensure_ascii=False, indent=2)

    print(f"\n[OK] 중간 데이터 저장 완료: {output_file}")
    print(f"   - 출력 디렉토리: {output_dir}")
    print(f"   - 총 {len(organized_data)}명 스토커 데이터")

    return organized_data

if __name__ == "__main__":
    main()
