

import os
import gc
import json
import time
import shutil
import tempfile
from pathlib import Path
from typing import Union, Optional


import pandas as pd
import geopandas as gpd
from shapely import wkt  


from qgis.core import (
    QgsProject,
    QgsVectorLayer,
    QgsGeometry,
)


from qgis.PyQt.QtWidgets import QMessageBox


import common  
from urbanq.logging.logging_config import logger  





def extract_most_common_file_type(folder_path):
    
    try:
        
        if not folder_path:
            QMessageBox.information(None, "오류", "폴더 경로가 설정되지 않았습니다.", QMessageBox.Ok)
            return []

        
        if not os.path.exists(folder_path):
            QMessageBox.information(None, "오류", f"폴더 경로가 유효하지 않습니다: {folder_path}", QMessageBox.Ok)
            return []

        
        txt_files = []
        csv_files = []

        for root, _, files in os.walk(folder_path):
            for file in files:
                if file.endswith(".txt"):
                    txt_files.append(os.path.join(root, file))
                elif file.endswith(".csv"):
                    csv_files.append(os.path.join(root, file))

        
        if not txt_files and not csv_files:
            QMessageBox.information(None, "정보", "폴더 내에 txt 또는 csv 파일이 없습니다.", QMessageBox.Ok)
            return []

        
        if len(txt_files) >= len(csv_files):
            return txt_files
        else:
            return csv_files

    except PermissionError as e:
        logger.error(f"폴더 접근 권한 오류: {e}", exc_info=True)
        QMessageBox.information(None, "오류", f"폴더 접근 권한이 없습니다: {folder_path}", QMessageBox.Ok)
        return []

    except Exception as e:
        logger.error(f"에러 발생: {e}", exc_info=True)
        QMessageBox.information(None, "오류", f"예기치 않은 오류가 발생했습니다: {e}", QMessageBox.Ok)
        return []





def load_txt_or_csv_preview_with_multiple_encodings(file_path, encoding_list, row_count=5, delimiter=',', has_header=True):
    
    
    if not os.path.isfile(file_path):
        logger.error("파일을 찾을 수 없습니다: %s", file_path)
        QMessageBox.information(None, "오류", f"파일을 찾을 수 없습니다: {file_path}", QMessageBox.Ok)
        return None, None, None

    try:
        
        with open(file_path, 'rb') as f:
            pass
    except IOError as e:
        logger.error("파일 접근 오류: %s", e)
        QMessageBox.information(None, "오류", f"'{file_path}' 파일 접근 오류: {e}", QMessageBox.Ok)
        return None, None, None

    
    header_param = 0 if has_header else None

    
    encoding_failed = True

    for encoding in encoding_list:
        try:
            
            data = pd.read_csv(file_path,
                               encoding=encoding,
                               nrows=row_count,
                               sep=delimiter,
                               header=header_param,
                               dtype=str,
                               keep_default_na=False,  
                               on_bad_lines='warn')  

            
            if has_header:
                header = list(map(str, data.columns.tolist()))
            else:
                header = [str(col) for col in data.columns]

            
            rows = data.fillna('').values.astype(str).tolist()

            
            encoding_failed = False

            return header, rows, encoding

        except UnicodeDecodeError as e:
            logger.warning("'%s' 인코딩 오류 발생: %s", encoding, e)

        except pd.errors.EmptyDataError:
            logger.warning("'%s' 인코딩으로 읽었으나 파일에 데이터가 없음", encoding)
            return None, None, encoding  

        except pd.errors.ParserError as e:
            logger.warning("'%s' 인코딩으로 읽는 중 구문 분석 오류 발생: %s", encoding, e)

        except Exception as e:
            logger.warning("'%s' 인코딩 중 예상치 못한 오류 발생: %s", encoding, e, exc_info=True)

    
    if encoding_failed:
        QMessageBox.information(None, "오류", f"지원되는 인코딩을 찾을 수 없습니다. 파일: {file_path}", QMessageBox.Ok)

    
    return None, None, None


def load_txt_or_csv_preview_with_single_encoding(file_path, encoding='UTF-8', row_count=5, delimiter=',', has_header=True):
    
    
    if not os.path.isfile(file_path):
        logger.error("파일을 찾을 수 없습니다: %s", file_path)
        QMessageBox.information(None, "오류", f"'{file_path}' 파일을 찾을 수 없습니다", QMessageBox.Ok)
        return None, None, None

    try:
        
        with open(file_path, 'rb') as f:
            pass

    except IOError as e:
        logger.error("파일 접근 오류: %s", e)
        QMessageBox.information(None, "오류", f"'{file_path}' 파일 접근 오류: %s {e}", QMessageBox.Ok)
        return None, None, None

    
    header_param = 0 if has_header else None

    try:
        
        data = pd.read_csv(file_path,
                           encoding=encoding,
                           nrows=row_count,
                           sep=delimiter,
                           header=header_param,
                           dtype=str,
                           keep_default_na=False,  
                           on_bad_lines='warn')  

        
        if has_header:
            header = list(map(str, data.columns.tolist()))
        else:
            header = [str(col) for col in data.columns]

        
        rows = data.fillna('').values.astype(str).tolist()

        return header, rows, encoding

    except UnicodeDecodeError as e:
        logger.warning("'%s' 인코딩 오류 발생: %s", encoding, e)
        QMessageBox.information(None, "오류", f"'{encoding}' 인코딩 오류 발생: {e}", QMessageBox.Ok)

    except pd.errors.EmptyDataError:
        logger.warning("'%s' 인코딩으로 읽었으나 파일에 데이터가 없음", encoding)
        QMessageBox.information(None, "오류", f"'{encoding}' 인코딩으로 읽었으나 파일에 데이터가 없습니다.", QMessageBox.Ok)
        return None, None, encoding  

    except pd.errors.ParserError as e:
        logger.warning("'%s' 인코딩으로 읽는 중 구문 분석 오류 발생: %s", encoding, e)
        QMessageBox.information(None, "오류", f"'{encoding}' 인코딩으로 읽는 중 구문 분석 오류 발생: {e}", QMessageBox.Ok)

    except Exception as e:
        logger.warning("'%s' 인코딩 중 예상치 못한 오류 발생: %s", encoding, e, exc_info=True)
        QMessageBox.information(None, "오류", f"예기치 않은 오류가 발생했습니다: {e}", QMessageBox.Ok)

    
    return None, None, None


def load_txt_or_csv_df(file_path, encoding='UTF-8', delimiter=',', has_header=True):
    
    
    if not os.path.isfile(file_path):
        logger.error("파일을 찾을 수 없습니다: %s", file_path)
        QMessageBox.information(None, "오류", f"'{file_path}' 파일을 찾을 수 없습니다", QMessageBox.Ok)
        return None

    try:
        
        with open(file_path, 'rb') as f:
            pass

    except IOError as e:
        logger.error("파일 접근 오류: %s", e)
        QMessageBox.information(None, "오류", f"'{file_path}' 파일 접근 오류: %s {e}", QMessageBox.Ok)
        return None

    
    header_param = 0 if has_header else None

    try:
        
        df = pd.read_csv(file_path,
                         encoding=encoding,
                         sep=delimiter,
                         header=header_param,
                         dtype=str,
                         keep_default_na=False,  
                         on_bad_lines='warn')  

        return df

    except UnicodeDecodeError as e:
        logger.warning("'%s' 인코딩 오류 발생: %s", encoding, e)
        QMessageBox.information(None, "오류", f"'{encoding}' 인코딩 오류 발생: {e}", QMessageBox.Ok)

    except pd.errors.EmptyDataError:
        logger.warning("'%s' 인코딩으로 읽었으나 파일에 데이터가 없음", encoding)
        QMessageBox.information(None, "오류", f"'{encoding}' 인코딩으로 읽었으나 파일에 데이터가 없습니다.", QMessageBox.Ok)
        return None

    except pd.errors.ParserError as e:
        logger.warning("'%s' 인코딩으로 읽는 중 구문 분석 오류 발생: %s", encoding, e)
        QMessageBox.information(None, "오류", f"'{encoding}' 인코딩으로 읽는 중 구문 분석 오류 발생: {e}", QMessageBox.Ok)

    except Exception as e:
        logger.warning("'%s' 인코딩 중 예상치 못한 오류 발생: %s", encoding, e, exc_info=True)
        QMessageBox.information(None, "오류", f"예기치 않은 오류가 발생했습니다: {e}", QMessageBox.Ok)

    
    return None





def load_json_or_geojson_preview(file_path, file_encoding=None, row_count=5):
    
    def detect_file_encoding(file_path, encodings_list):
        
        for enc in encodings_list:
            try:
                with open(file_path, 'r', encoding=enc) as f:
                    f.read()  
                return enc  
            except UnicodeDecodeError:
                continue
        return 'UTF-8'  

    try:
        if not os.path.exists(file_path):
            QMessageBox.information(None, "오류", f"파일이 존재하지 않습니다: {file_path}", QMessageBox.Ok)
            return None, None, None, None

        if not os.access(file_path, os.R_OK):
            QMessageBox.information(None, "오류", f"파일에 접근할 수 없습니다: {file_path}", QMessageBox.Ok)
            return None, None, None, None

        
        if not file_encoding:
            file_encoding = detect_file_encoding(file_path, common.korea_encodings)

        
        with open(file_path, 'r', encoding=file_encoding) as json_file:
            first_chars = json_file.read(10).strip()  
            json_file.seek(0)  

            
            if first_chars.startswith("["):
                json_data = json.load(json_file)  
                if not json_data:
                    QMessageBox.information(None, "오류", "JSON 데이터가 비어 있습니다.", QMessageBox.Ok)
                    return None, None, None, None

                if isinstance(json_data, list) and isinstance(json_data[0], dict):
                    header = list(json_data[0].keys())
                    rows = [list(item.values()) for item in json_data[:row_count]]
                    return header, rows, file_encoding, "json"

            
            else:
                json_data = json.load(json_file)

                
                if isinstance(json_data, dict) and json_data.get("type") == "FeatureCollection":
                    features = json_data.get("features", [])
                    if not features:
                        QMessageBox.information(None, "오류", "GeoJSON에 features가 없습니다.", QMessageBox.Ok)
                        return None, None, None, None

                    first_feature = features[0]
                    if "properties" in first_feature:
                        header = list(first_feature["properties"].keys())
                        rows = [
                            [str(feature["properties"].get(key, "")) for key in header]
                            for feature in features[:row_count]
                        ]
                        return header, rows, file_encoding, "geojson"

                
                elif isinstance(json_data, dict):
                    header = list(json_data.keys())
                    rows = [[str(json_data.get(key, '')) for key in header]]
                    return header, rows, file_encoding, "json"

        QMessageBox.information(None, "오류", "지원되지 않는 JSON 형식입니다.", QMessageBox.Ok)
        return None, None, None, None

    except json.JSONDecodeError as e:
        QMessageBox.information(None, "오류", f"JSON 파일이 손상되었거나 유효하지 않습니다: {e}", QMessageBox.Ok)
        return None, None, None, None

    except Exception as e:
        QMessageBox.information(None, "오류", f"파일 처리 중 오류 발생: {e}", QMessageBox.Ok)
        return None, None, None, None

    finally:
        gc.collect()  


def load_json_df_or_gdf(file_path, file_encoding="UTF-8"):
    
    def is_geojson(file_path, encoding="utf-8"):
        
        try:
            with open(file_path, 'r', encoding=encoding) as f:
                data = json.load(f)

            
            if isinstance(data, dict):
                if data.get("type") == "FeatureCollection" and "features" in data:
                    return True
            return False

        except Exception as e:
            QMessageBox.information(None, "오류", f"[ERROR] JSON 판별 중 오류 발생: {e}", QMessageBox.Ok)
            return False

    try:
        if not os.path.exists(file_path):
            QMessageBox.information(None, "오류", f"파일이 존재하지 않습니다: {file_path}", QMessageBox.Ok)
            return None

        if not os.access(file_path, os.R_OK):
            QMessageBox.information(None, "오류", f"파일에 접근할 수 없습니다: {file_path}", QMessageBox.Ok)
            return None

        
        file_encoding = "CP949" if file_encoding.lower().replace("-", "").replace("_", "") in ("windows949", "win949", "949") else file_encoding

        
        if is_geojson(file_path, file_encoding):
            
            
            
            return None, load_geojson_gdf(file_path, file_encoding)

        with open(file_path, 'r', encoding=file_encoding) as json_file:
            json_data = json.load(json_file)

        
        if isinstance(json_data, list) and all(isinstance(item, dict) for item in json_data):
            df = pd.DataFrame(json_data)

        
        elif isinstance(json_data, dict):
            df = pd.DataFrame([json_data])  

        else:
            QMessageBox.information(None, "오류", "지원되지 않는 JSON 형식입니다.", QMessageBox.Ok)
            return None

        if df.empty:
            QMessageBox.information(None, "오류", "DataFrame이 비어 있습니다.", QMessageBox.Ok)
            return None

        
        
        
        return df, None

    except Exception as e:
        QMessageBox.information(None, "오류", f"파일 처리 중 오류 발생: {e}", QMessageBox.Ok)
        return None

    finally:
        gc.collect()


def load_geojson_gdf(file_path, file_encoding="UTF-8"):
    
    try:
        if not os.path.exists(file_path):
            QMessageBox.information(None, "오류", f"파일이 존재하지 않습니다: {file_path}", QMessageBox.Ok)
            return None

        if not os.access(file_path, os.R_OK):
            QMessageBox.information(None, "오류", f"파일에 접근할 수 없습니다: {file_path}", QMessageBox.Ok)
            return None

        file_encoding = "CP949" if file_encoding.lower().replace("-", "").replace("_", "") in ("windows949", "win949", "949") else file_encoding

        gdf = gpd.read_file(file_path, encoding=file_encoding)

        if not isinstance(gdf, gpd.GeoDataFrame) or gdf.empty:
            QMessageBox.information(None, "오류", "GeoDataFrame이 비어 있습니다.", QMessageBox.Ok)
            return None

        if gdf.crs is None:
            gdf.set_crs("EPSG:4326", inplace=True)

        return gdf

    except Exception as e:
        QMessageBox.information(None, "오류", f"파일 처리 중 오류 발생: {e}", QMessageBox.Ok)
        return None

    finally:
        gc.collect()





def load_layer_or_shp_preview(layer=None, file_path=None, file_encoding=None, row_count=5):
    
    def encode_text(text, source_encoding, target_encoding):
        
        if not target_encoding:
            return text
        return text.encode(source_encoding, errors='replace').decode(target_encoding, errors='replace')

    def get_cpg_encoding(file_path):
        
        cpg_path = file_path.replace('.shp', '.cpg')
        if os.path.exists(cpg_path):
            with open(cpg_path, 'r') as cpg_file:
                encoding = cpg_file.read().strip()
                return encoding
        return None

    try:
        
        
        if file_path:
            layer = QgsVectorLayer(file_path, 'test', "ogr")

        
        if not layer or not layer.isValid():
            logger.error("잘못된 레이어: %s", file_path or getattr(layer, 'name', 'unknown'))
            QMessageBox.information(None, "오류", f"잘못된 레이어입니다.", QMessageBox.Ok)
            return None, None, None

        
        if layer.featureCount() == 0:
            logger.error("빈 레이어: %s", file_path or layer.name())
            QMessageBox.information(None, "오류", f"레이어가 비었습니다.", QMessageBox.Ok)
            return None, None, None

        
        encoding = None

        if file_path:
            cpg_encoding = get_cpg_encoding(file_path)
            if cpg_encoding and cpg_encoding in common.korea_encodings:
                encoding = cpg_encoding
        else:
            cpg_encoding = get_cpg_encoding(layer.source())
            if cpg_encoding and cpg_encoding in common.korea_encodings:
                encoding = cpg_encoding

        if not encoding:
            encoding = layer.dataProvider().encoding() or "UTF-8"

        
        encoding = "CP949" if encoding.lower().replace("-", "").replace("_", "") in ("windows949", "win949", "949") else encoding

        
        try:
            header = [encode_text(field.name(), encoding, file_encoding) for field in layer.fields()]

        except UnicodeError as e:
            logger.error("필드 이름 인코딩 실패: %s", e)
            QMessageBox.information(None, "오류", f"필드 이름 인코딩 실패했습니다: {e}", QMessageBox.Ok)
            return None, None, None

        except AttributeError as e:
            logger.error("필드 이름 추출 실패: %s", e)
            QMessageBox.information(None, "오류", f"필드 이름 추출 실패했습니다: {e}", QMessageBox.Ok)
            return None, None, None

        
        rows = []
        try:
            
            features = layer.getFeatures()
            for i, feat in enumerate(features):
                if i >= row_count:  
                    break
                
                row = []
                for attr in feat.attributes():
                    if isinstance(attr, str):
                        row.append(encode_text(attr, encoding, file_encoding))
                    else:
                        row.append(attr)  
                rows.append(row)

        except UnicodeError as e:
            logger.error("데이터 인코딩 실패: %s", e)
            QMessageBox.information(None, "오류", f"데이터 인코딩 실패했습니다: {e}", QMessageBox.Ok)
            return None, None, None

        except AttributeError as e:
            logger.error("속성 데이터 추출 실패: %s", e)
            QMessageBox.information(None, "오류", f"속성 데이터 추출 실패했습니다: {e}", QMessageBox.Ok)
            return None, None, None

        except Exception as e:
            logger.error("행 데이터 처리 중 오류 발생: %s", e)
            QMessageBox.information(None, "오류", f"행 데이터 처리 중 오류 발생했습니다: {e}", QMessageBox.Ok)
            return None, None, None

        return header, rows, file_encoding if file_encoding else encoding

    except Exception as e:
        logger.error("에러 발생: %s", e, exc_info=True)
        QMessageBox.information(None, "오류", f"예기치 않은 오류가 발생했습니다: {e}", QMessageBox.Ok)
        return None, None, None

    finally:
        gc.collect()  


def load_layer_or_shp_gdf(layer=None, shp_path=None, file_encoding=None):
    

    def encode_text(text, source_encoding, target_encoding):
        
        if not isinstance(text, str):
            return text
        if not target_encoding or not source_encoding:
            return text
        try:
            return text.encode(source_encoding, errors='replace').decode(target_encoding, errors='replace')
        except Exception as e:
            logger.error("인코딩 변환 실패: %s", e, exc_info=True)
            return text

    def get_cpg_encoding(file_path):
        
        if not file_path or not file_path.endswith('.shp'):
            return None
        cpg_path = file_path.replace('.shp', '.cpg')
        if os.path.exists(cpg_path):
            try:
                with open(cpg_path, 'r') as cpg_file:
                    return cpg_file.read().strip()
            except Exception as e:
                logger.error("CPG 파일 읽기 실패: %s", e, exc_info=True)
        return None

    def get_encoding_from_layer_or_path(layer=None, path=None):
        if path:
            return get_cpg_encoding(path)
        elif layer:
            enc = get_cpg_encoding(layer.source().split('|')[0])
            if enc:
                return enc
            return layer.dataProvider().encoding()
        return "UTF-8"

    def is_shp_layer(layer):
        try:
            return layer.source().split('|')[0].lower().endswith('.shp')
        except Exception:
            return False

    def layer_to_gdf(layer, layer_encoding="UTF-8"):
        

        
        if not layer or not layer.isValid():
            QMessageBox.information(None, "오류", "잘못된 레이어입니다.", QMessageBox.Ok)
            return None

        if layer.featureCount() == 0:
            QMessageBox.information(None, "오류", "레이어가 비었습니다.", QMessageBox.Ok)
            return None

        
        encoding = None
        cpg_encoding = get_cpg_encoding(layer.source())
        if cpg_encoding:
            encoding = cpg_encoding

        if not encoding:
            encoding = layer.dataProvider().encoding() or layer_encoding

        
        encoding = "CP949" if encoding.lower().replace("-", "").replace("_", "") in ("windows949", "win949", "949") else encoding

        
        try:
            header = [encode_text(field.name(), encoding, file_encoding) for field in layer.fields()]

        except UnicodeError as e:
            logger.error("필드 이름 인코딩 실패: %s", e)
            QMessageBox.information(None, "오류", f"필드 이름 인코딩 실패했습니다: {e}", QMessageBox.Ok)
            return None

        except AttributeError as e:
            logger.error("필드 이름 추출 실패: %s", e)
            QMessageBox.information(None, "오류", f"필드 이름 추출 실패했습니다: {e}", QMessageBox.Ok)
            return None

        
        rows = []
        geometries = []
        try:
            features = layer.getFeatures()
            for feat in features:
                
                row = []
                for attr in feat.attributes():
                    if isinstance(attr, str):
                        row.append(encode_text(attr, encoding, file_encoding))
                    else:
                        row.append(attr)
                rows.append(row)

                
                geom = feat.geometry()
                if geom and not geom.isNull():
                    geometries.append(QgsGeometry(geom))
                else:
                    geometries.append(None)

        except UnicodeError as e:
            logger.error("데이터 인코딩 실패: %s", e)
            QMessageBox.information(None, "오류", f"데이터 인코딩 실패했습니다: {e}", QMessageBox.Ok)
            return None

        except AttributeError as e:
            logger.error("속성 데이터 추출 실패: %s", e)
            QMessageBox.information(None, "오류", f"속성 데이터 추출 실패했습니다: {e}", QMessageBox.Ok)
            return None

        except Exception as e:
            logger.error("행 데이터 처리 중 오류 발생: %s", e)
            QMessageBox.information(None, "오류", f"행 데이터 처리 중 오류 발생했습니다: {e}", QMessageBox.Ok)
            return None

        
        try:
            df = pd.DataFrame(rows, columns=header)

            
            wkt_geoms = [geom.asWkt() if geom else None for geom in geometries]
            df['geometry'] = [wkt.loads(wkt_geom) if wkt_geom else None for wkt_geom in wkt_geoms]

            gdf = gpd.GeoDataFrame(df, geometry='geometry', crs=layer.crs().toWkt())

            return gdf

        except Exception as e:
            logger.error("GeoDataFrame 생성 실패: %s", e)
            QMessageBox.information(None, "오류", f"GeoDataFrame 생성에 실패했습니다: {e}", QMessageBox.Ok)
            return None

    def shp_to_gdf(shp_path, shp_encoding="UTF-8"):
        
        if not shp_path or not os.path.exists(shp_path):
            QMessageBox.warning(None, "경고", f"SHP 파일 경로가 유효하지 않습니다: {shp_path}", QMessageBox.Ok)
            return None

        
        shp_encoding = "CP949" if shp_encoding.lower().replace("-", "").replace("_", "") in ("windows949", "win949", "949") else shp_encoding

        try:
            gdf = gpd.read_file(shp_path, encoding=shp_encoding)
            if not isinstance(gdf, gpd.GeoDataFrame) or gdf.empty:
                QMessageBox.warning(None, "경고", f"SHP 파일이 유효하지 않거나 비어 있습니다: {shp_path}", QMessageBox.Ok)
                return None
            return gdf

        except UnicodeDecodeError as err:
            logger.error(f"인코딩 오류 [{shp_encoding}]: {shp_path}", err, exc_info=True)
            QMessageBox.information(
                None,
                "Shapefile 불러오기 안내",
                "Shapefile(.shp) 파일을 불러오는 과정에서 오류가 발생했습니다.\n"
                "먼저 파일 인코딩을 변경한 후 다시 시도해 보시기 바랍니다.\n"
                "문제가 해결되지 않는 경우, 아래와 같은 원인일 수 있습니다.\n\n"

                "1. 문자 인코딩 오류 (가장 흔한 원인)\n"
                "   - 실제 파일 인코딩과 선택한 인코딩이 서로 다를 경우\n\n"

                "2. 필드명 문제\n"
                "   - 필드명에 '��', 깨진 한글, 특수문자가 포함된 경우\n"
                "   - Shapefile 규격상 필드명 길이 제한(10바이트)을 초과한 경우\n\n"

                "3. 좌표 데이터 문제\n"
                "   - 경도/위도 필드에 문자열, 공백, 누락값이 포함된 경우\n"
                "   - 좌표계(CRS) 정보가 없거나 잘못 지정된 경우\n\n"

                "4. Shapefile 구성 파일 누락 또는 손상\n"
                "   - .shp, .dbf, .shx 파일 중 일부가 없거나 손상된 경우\n\n"

                "※ 권장 해결 방법\n"
                " - 파일을 UTF-8 인코딩으로 저장한 후 다시 불러오기\n"
            )
            return None

        except Exception as err:
            logger.error(f"SHP 파일 처리 실패: {shp_path}", err, exc_info=True)
            QMessageBox.warning(None, "경고", f"SHP 파일 처리 실패했습니다: {err}", QMessageBox.Ok)
            return None

    try:
        encoding = file_encoding or get_encoding_from_layer_or_path(layer, shp_path)

        
        if shp_path:
            return shp_to_gdf(shp_path, encoding)

        
        elif is_shp_layer(layer):
            return shp_to_gdf(layer.source().split('|')[0], encoding)

        
        else:
            return layer_to_gdf(layer, encoding)

    except Exception as e:
        logger.error("예기치 않은 오류 발생: %s", e, exc_info=True)
        QMessageBox.information(None, "오류", f"처리 중 오류 발생: {e}", QMessageBox.Ok)
        return None

    finally:
        gc.collect()


def update_shapefile_layer(original_layer, updated_gdf):
    
    try:
        
        old_path = Path(original_layer.source())
        name = original_layer.name()

        
        layers = list(QgsProject.instance().layerTreeRoot().children())
        layer_order = [layer.layerId() for layer in layers]
        target_index = layer_order.index(original_layer.id()) if original_layer.id() in layer_order else -1

        
        temp_dir = tempfile.mkdtemp()
        temp_shp = Path(temp_dir) / (name + ".shp")
        updated_gdf.to_file(temp_shp, encoding='utf-8')

        
        QgsProject.instance().removeMapLayer(original_layer.id())
        time.sleep(0.5)

        
        for ext in ['.shp', '.shx', '.dbf', '.prj', '.cpg', '.qpj']:
            f = old_path.with_suffix(ext)
            if f.exists():
                try:
                    f.unlink()
                except:
                    print(f"❗ 삭제 실패 (점유 중): {f}")

        
        for ext in ['.shp', '.shx', '.dbf', '.prj']:
            src = temp_shp.with_suffix(ext)
            dst = old_path.with_suffix(ext)
            if src.exists():
                shutil.copy(src, dst)

        
        new_layer = QgsVectorLayer(str(old_path), name, "ogr")
        QgsProject.instance().addMapLayer(new_layer, addToLegend=False)

        
        root = QgsProject.instance().layerTreeRoot()
        root.insertLayer(target_index, new_layer)

        return new_layer

    except Exception as e:
        logger.error("예기치 않은 오류 발생: %s", e, exc_info=True)





def export_gdf(
        gdf: gpd.GeoDataFrame,
        output_path: Union[str, Path],
        source_encoding: Optional[str] = None,
        index: bool = False,
        driver: Optional[str] = None,
        **kwargs
) -> bool:
    

    try:
        
        if (
                gdf is None
                or not isinstance(gdf, gpd.GeoDataFrame)
                or gdf.empty
        ):
            return False

        output_path = Path(output_path)
        ext = output_path.suffix.lower()

        
        needs_conversion = source_encoding and source_encoding.lower() not in (
            'utf-8', 'utf8', 'utf_8'
        )

        if needs_conversion:
            str_cols = [
                col for col in gdf.columns
                if pd.api.types.is_string_dtype(gdf[col])
            ]

            for col in str_cols:
                mask = gdf[col].notna()
                if mask.any():
                    gdf.loc[mask, col] = (
                        gdf.loc[mask, col]
                        .str.encode(source_encoding)
                        .str.decode('utf-8')
                    )

        
        spatial_formats = {
            '.geojson': 'GeoJSON',
            '.gpkg': 'GPKG',
            '.shp': 'ESRI Shapefile'
        }

        if ext in spatial_formats or driver is not None:
            driver = driver or spatial_formats.get(ext)

            if ext == '.geojson':
                gdf.to_file(output_path, driver='GeoJSON', **kwargs)

            elif ext == '.shp':
                gdf.to_file(
                    output_path,
                    driver='ESRI Shapefile',
                    encoding='utf-8',
                    **kwargs
                )

        
        else:
            df = pd.DataFrame(
                gdf.drop(columns='geometry', errors='ignore')
            )

            if ext == '.txt':
                df.to_csv(
                    output_path,
                    index=index,
                    sep='|',
                    encoding='utf-8-sig',
                    **kwargs
                )

            elif ext == '.csv':
                df.to_csv(
                    output_path,
                    index=index,
                    encoding='utf-8-sig',
                    **kwargs
                )

            elif ext == '.json':
                df.to_json(
                    output_path,
                    orient='records',
                    force_ascii=False,
                    **kwargs
                )

            else:
                return False

        
        return True

    except Exception as e:
        logger.error("오류 발생: %s", e, exc_info=True)
        QMessageBox.information(
            None,
            "파일 저장 실패",
            "GeoDataFrame을 파일로 저장하는 중 오류가 발생했습니다.\n\n"
            "다음 사항을 확인해 주세요:\n"
            "• 출력 경로가 올바른지\n"
            "• 파일이 다른 프로그램에서 열려 있지 않은지\n"
            "• 해당 위치에 쓰기 권한이 있는지\n\n"
            "문제가 지속되면 관리자에게 문의해 주세요.",
            QMessageBox.Ok
        )
        return False





def keep_columns_df(df, columns_to_keep):
    
    return df.loc[:, df.columns.intersection(columns_to_keep)]


def keep_columns_gdf(gdf, columns_to_keep):
    
    columns_set = set(columns_to_keep)
    if gdf.geometry.name not in columns_set:
        columns_set.add(gdf.geometry.name)
    return gdf.loc[:, gdf.columns.intersection(columns_set)]


def df_to_empty_geometry_gdf(df: pd.DataFrame, crs: str = "EPSG:4326") -> gpd.GeoDataFrame:
    
    df = df.copy()
    df["geometry"] = None  
    gdf = gpd.GeoDataFrame(df, geometry="geometry", crs=crs)
    return gdf

