import pandas as pd
import numpy as np
from typing import Union, List


def make_merge_ins(ins_df, *merge_dfs, df_names=None, silent=False):
    """
    ins_df를 기준으로 ID별 집계된 데이터프레임들을 left join하는 함수.
    
    in_ith_mon, in_ith_year, kcd_ith_mon, kcd_ith_year 같은 ID별로 집계된 데이터프레임을
    ins_df에 붙여주는 함수. ins_df가 기준으로 left join을 수행하며,
    붙이는 도중에 결측값이 생기면 어떤 데이터프레임을 붙이다가 발생했는지 출력.
    결측값은 숫자 99로 처리.
    
    Parameters:
    -----------
    ins_df : pd.DataFrame
        기준 데이터프레임. 'ID' 컬럼이 있어야 함
    *merge_dfs : pd.DataFrame
        병합할 데이터프레임들 (가변 인자). 각 데이터프레임은 'ID' 컬럼을 가져야 함
    df_names : list of str, optional
        각 merge_df의 이름 리스트. None이면 자동으로 'df1', 'df2', ... 형태로 부여.
        결측값 발생 시 어떤 데이터프레임인지 식별하는 데 사용.
    
    Returns:
    --------
    pd.DataFrame
        ins_df에 모든 merge_dfs를 left join한 결과.
        결측값은 99로 처리됨.
    
    Examples:
    ---------
    >>> result = in_ith_mon(claims_df, '2018-01-01', 3)
    >>> result2 = kcd_ith_year(claims_df, '2018-01-01', [1, 3], {'brain': r'I6[0-9]'})
    >>> merged = make_merge_ins(ins_df, result, result2, 
    ...                         df_names=['in_ith_mon_3m', 'kcd_ith_year_brain'])
    """
    # 입력 검증
    if 'ID' not in ins_df.columns:
        raise ValueError("ins_df에 'ID' 컬럼이 없습니다.")
    
    if len(merge_dfs) == 0:
        return ins_df.copy()
    
    # df_names 처리
    if df_names is None:
        df_names = [f'df{i+1}' for i in range(len(merge_dfs))]
    elif len(df_names) != len(merge_dfs):
        raise ValueError(f"df_names의 길이({len(df_names)})와 merge_dfs의 개수({len(merge_dfs)})가 일치하지 않습니다.")
    
    # ins_df 복사 (원본 수정 방지)
    result_df = ins_df.copy()
    
    # 각 데이터프레임 순차적으로 병합
    for i, (merge_df, df_name) in enumerate(zip(merge_dfs, df_names)):
        # 입력 검증
        if not isinstance(merge_df, pd.DataFrame):
            raise TypeError(f"{df_name}이(가) pandas DataFrame이 아닙니다.")
        
        if 'ID' not in merge_df.columns:
            raise ValueError(f"{df_name}에 'ID' 컬럼이 없습니다.")
        
        # 병합 전 상태 저장 (결측값 체크용)
        before_cols = set(result_df.columns)
        
        # ID 컬럼을 제외한 병합할 컬럼들
        merge_cols = [col for col in merge_df.columns if col != 'ID']
        
        if len(merge_cols) == 0:
            if not silent:
                print(f"[경고] {df_name}: 병합할 컬럼이 없습니다 (ID 컬럼만 존재). 건너뜁니다.")
            continue
        
        # 중복 컬럼명 체크 (ID 제외)
        duplicate_cols = set(result_df.columns) & set(merge_cols)
        if duplicate_cols:
            if not silent:
                print(f"[경고] {df_name}: 다음 컬럼들이 이미 존재합니다: {list(duplicate_cols)}")
                print(f"       기존 컬럼은 유지되고 새로운 컬럼은 병합되지 않습니다.")
            merge_cols = [col for col in merge_cols if col not in duplicate_cols]
            if len(merge_cols) == 0:
                if not silent:
                    print(f"[경고] {df_name}: 모든 컬럼이 중복되어 건너뜁니다.")
                continue
        
        # Left join 수행
        result_df = result_df.merge(
            merge_df[['ID'] + merge_cols],
            on='ID',
            how='left'
        )
        
        # 병합 후 새로 추가된 컬럼들
        after_cols = set(result_df.columns)
        new_cols = after_cols - before_cols
        
        # 새로 추가된 컬럼들에 대해 결측값 확인
        if new_cols:
            for col in new_cols:
                missing_count = result_df[col].isna().sum()
                if missing_count > 0:
                    missing_ratio = (missing_count / len(result_df)) * 100
                    if not silent:
                        print(f"[결측값 발견] {df_name} 병합 후 '{col}' 컬럼: "
                              f"{missing_count:,}개 ({missing_ratio:.2f}%) 결측값 발생 → 99로 처리")
            
            # 숫자형 컬럼에 대해서만 결측값을 99로 처리
            # 모든 새로 추가된 컬럼을 숫자형으로 가정 (in_ith_mon, kcd_ith_mon 등은 0/1 반환)
            for col in new_cols:
                if result_df[col].dtype in ['int64', 'int32', 'float64', 'float32']:
                    result_df[col] = result_df[col].fillna(99)
                else:
                    # 숫자형이 아닌 경우에도 시도 (예: int8 등)
                    try:
                        result_df[col] = pd.to_numeric(result_df[col], errors='coerce').fillna(99)
                    except:
                        print(f"[경고] {df_name}: '{col}' 컬럼을 숫자형으로 변환할 수 없습니다. 결측값을 그대로 유지합니다.")
    
    return result_df

