
import os
import re
from pathlib import Path


from qgis.PyQt import uic, QtWidgets
from qgis.PyQt.QtWidgets import QApplication, QDialog, QMessageBox, QWidget


from qgis.core import QgsVectorLayer


import numpy as np
import pandas as pd
import geopandas as gpd
from shapely.geometry.base import BaseGeometry


import common
from urbanq.logging.logging_config import logger
from urbanq.function.qss import gradient_style, default_style


from urbanq.function.file import (
    export_gdf,
    keep_columns_gdf,
    load_geojson_gdf,
    load_txt_or_csv_df,
    load_json_df_or_gdf,
    load_layer_or_shp_gdf,
    update_shapefile_layer,
    df_to_empty_geometry_gdf,
)

from urbanq.function.geo import (
    is_real_null,
    is_empty_value,
    normalize_null_values,
)

from urbanq.function.widgetutils import (
    show_progress,
    update_progress,
)


from urbanq.menu.autoUI.fileRread_dockwidget import fileRreadDockWidget
from urbanq.menu.autoUI.fileSave_dockwidget import fileSaveDockWidget
from urbanq.menu.autoUI.fileSetting_dockwidget import fileSettingDockWidget
from urbanq.menu.autoUI.ImageDescription_dockwidget import ImageDescriptionDockWidget



FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'DataQualityCheck_dockwidget_base.ui'))


class DataQualityCheckDockWidget(QDialog, FORM_CLASS):  
    def __init__(self, parent=None):
        
        super(DataQualityCheckDockWidget, self).__init__(parent)  
        
        
        
        
        
        self.setupUi(self)

        
        show_progress(self.progressBar, False)

        
        self.menuPushButton.setProperty("class", "boldText")
        self.nextStepPushButton.setProperty("class", "boldText")
        self.previousStepPushButton.setProperty("class", "boldText")

        
        self.menuPushButton.clicked.connect(self.go_back_to_data_conversion)

        
        self.nextStepPushButton.clicked.connect(lambda: self.next_previous_clicked(1))
        self.nextStepPushButton.clicked.connect(lambda: self.update_current_progress(self.stackedWidget.currentIndex()))
        self.nextStepPushButton.clicked.connect(lambda: self.load_menu_ui(self.stackedWidget.currentIndex()))

        
        self.previousStepPushButton.clicked.connect(lambda: self.next_previous_clicked(-1))
        self.previousStepPushButton.clicked.connect(lambda: self.update_current_progress(self.stackedWidget.currentIndex()))
        self.previousStepPushButton.clicked.connect(lambda: self.load_menu_ui(self.stackedWidget.currentIndex()))

        
        self.job_index = common.job_info.get("job_index") if common.job_info else None
        self.job_title = common.job_info.get("job_title") if common.job_info else None

        
        self.option = self.get_widget_option(self.job_index, self.job_title)

        
        self.pages_and_files = self.configure_pages_and_files()

        
        self.update_current_progress(0)

        
        self.stackedWidget.setCurrentIndex(0)

        
        self.load_menu_ui(0)

    
    
    

    def configure_pages_and_files(self):
        
        try:
            pages = []

            
            pages.append((True, self.current_step_1, ImageDescriptionDockWidget, None, None))

            
            pages.append((True, self.current_step_2, fileRreadDockWidget, self.option, None))

            
            read_required = any([
                self.option["setting_by_text"],
                self.option["setting_by_array"],
                self.option["setting_by_expression"],
                self.option["setting_by_section"]["enabled"],
                self.option["setting_by_numeric"]["enabled"],
                self.option["setting_by_combo"]["enabled"],
            ])
            pages.append((read_required, self.current_step_3, fileSettingDockWidget, self.option, None))

            
            save_required = any([
                self.option["output_by_file"],
                self.option["output_by_field"],
                self.option["output_by_table"]
            ])
            pages.append((save_required, self.current_step_4, fileSaveDockWidget, self.option, None))

            return pages

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def go_back_to_data_conversion(self):
        
        try:
            from urbanq.menu.dataConversion.dataConversion_dockwidget import dataConversionDockWidget  
            parent_ui = dataConversionDockWidget(self)  
            main_page_layout = self.parent().parent().findChild(QWidget, "page_dataConversion").layout()
            if main_page_layout:
                
                for i in reversed(range(main_page_layout.count())):
                    main_page_layout.itemAt(i).widget().deleteLater()
                main_page_layout.addWidget(parent_ui)

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def load_menu_ui(self, index):
        
        try:
            widget_enabled, widget_process, widget_class, widget_option, widget_instance = self.pages_and_files[index]
            page = self.stackedWidget.widget(index)

            
            if widget_instance is None:

                
                widget_instance = widget_class(self, self.option)
                page.layout().addWidget(widget_instance)
                self.pages_and_files[index] = (
                    self.pages_and_files[index][0],
                    self.pages_and_files[index][1],
                    self.pages_and_files[index][2],
                    self.pages_and_files[index][3],
                    widget_instance
                )

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def update_current_progress(self, index):
        
        try:
            step = 1
            for i, (widget_enabled, widget_process, _, _, _) in enumerate(self.pages_and_files):
                if not widget_enabled:
                    widget_process.hide()
                    continue
                else:
                    updated_text = re.sub(r"\[\d+단계\]", f"[{step}단계]", widget_process.text())
                    widget_process.setText(updated_text)
                    step += 1

                
                widget_process.show()

                if i == index:
                    widget_process.setStyleSheet(gradient_style)
                else:
                    widget_process.setStyleSheet(default_style)

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def get_safe_page_index(self, current_index: int, direction: int) -> int:
        
        try:
            new_index = current_index

            while True:
                
                new_index += direction

                
                new_index = max(0, min(new_index, len(self.pages_and_files) - 1))

                
                if self.pages_and_files[new_index][0]:
                    return new_index

                
                if new_index == 0 and direction == -1:
                    return current_index

                
                if new_index == len(self.pages_and_files) - 1 and direction == 1:
                    return current_index

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def next_previous_clicked(self, direction):
        
        def get_last_valid_page_index(pages_and_files) -> int:
            
            for i in reversed(range(len(pages_and_files))):
                if pages_and_files[i][0]:
                    return i
            return -1  

        try:
            
            current_index = self.stackedWidget.currentIndex()

            
            if self.pages_and_files[current_index][0]:
                instance = self.pages_and_files[current_index][4]
                if direction > 0 and not instance.set_fileResults():
                    return

            
            new_index = self.get_safe_page_index(current_index, direction)

            
            last_page_index = get_last_valid_page_index(self.pages_and_files)

            
            self.nextStepPushButton.setText("실행하기 " if new_index == last_page_index else "다음 단계 ▶")

            
            self.stackedWidget.setCurrentIndex(new_index)

            
            if current_index == last_page_index and direction > 0:
                self.run_job_process()

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    
    
    

    def get_file_data_frame(self, source_file_type, source_file_path, file_path, file_encoding, file_delimiter, file_has_header):
        
        try:
            
            gdf = None

            
            if source_file_type == "shp":
                gdf = load_layer_or_shp_gdf(shp_path=file_path, file_encoding=file_encoding)

            
            elif source_file_type == "layer":
                qgs_project_layer = source_file_path
                gdf = load_layer_or_shp_gdf(layer=qgs_project_layer, file_encoding=file_encoding)

            
            elif source_file_type == "json":
                df, _ = load_json_df_or_gdf(file_path=file_path, file_encoding=file_encoding)
                gdf = df_to_empty_geometry_gdf(df)

            
            elif source_file_type == "geojson":
                gdf = load_geojson_gdf(file_path=file_path, file_encoding=file_encoding)

            
            elif source_file_type == "txt":
                df = load_txt_or_csv_df(file_path, file_encoding, file_delimiter, file_has_header)
                gdf = df_to_empty_geometry_gdf(df)

            
            elif source_file_type == "csv":
                df = load_txt_or_csv_df(file_path, file_encoding, file_delimiter, file_has_header)
                gdf = df_to_empty_geometry_gdf(df)

            
            elif source_file_type == "folder":
                df = load_txt_or_csv_df(file_path, file_encoding, file_delimiter, file_has_header)
                gdf = df_to_empty_geometry_gdf(df)

            if gdf is None:
                return

            return gdf

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def run_job_process(self):
        
        try:
            
            show_progress(self.progressBar)

            
            total_files = len(common.fileInfo_1.file_preview)  
            steps_per_file = 4  
            total_steps = total_files * steps_per_file  
            base_progress = 20  
            step_weight = (100 - base_progress) / total_steps  
            current_step = 0  

            
            source_file_type, source_file_path, _ = common.fileInfo_1.file_record.get_record()
            result_file_type, result_file_path, _ = common.fileInfo_1.result_record.get_record()

            
            status_flags = []  
            for index, file_preview in enumerate(common.fileInfo_1.file_preview):

                
                file_path, file_encoding, file_delimiter, file_has_header = file_preview.get_info()
                current_step += 1
                update_progress(self.progressBar, int(base_progress + current_step * step_weight))

                
                if source_file_type == "folder":
                    
                    file_name_with_ext = os.path.basename(file_path)
                    new_file_path = os.path.join(result_file_path, file_name_with_ext)
                elif result_file_type == "layer":
                    new_file_path = file_path
                else:
                    new_file_path = result_file_path

                
                gdf = self.get_file_data_frame(source_file_type, source_file_path, file_path, file_encoding, file_delimiter, file_has_header)
                current_step += 1
                update_progress(self.progressBar, int(base_progress + current_step * step_weight))

                
                result = self.run_job_by_index(gdf, index)
                current_step += 1
                update_progress(self.progressBar, int(base_progress + current_step * step_weight))

                
                if result is None:
                    status_flags.append(False)
                    break
                elif result is True:
                    
                    
                    status_flags.append(True)

                try:
                    
                    if result_file_type == 'layer':

                        
                        layer_widget = self.pages_and_files[1][4].get_qgs_layer_widget()

                        
                        layer_widget_index = layer_widget.currentIndex()

                        
                        layer = source_file_path

                        
                        new_layer = update_shapefile_layer(layer, result)

                        
                        if 0 <= layer_widget_index < layer_widget.count():
                            layer_widget.setCurrentIndex(layer_widget_index)

                        
                        common.fileInfo_1.file_record.file_path[result_file_type] = new_layer

                        
                        status_flags.append(True)

                    else:
                        
                        if new_file_path:

                            
                            if isinstance(result, gpd.GeoDataFrame):
                                export_success = export_gdf(result, new_file_path)

                                
                                status_flags.append(export_success)

                            elif isinstance(result, list) and result:
                                
                                file_type, _, file_name = common.fileInfo_1.file_record.get_record()
                                base_dir = Path(new_file_path)
                                base_name = Path(file_name).stem
                                ext = f".{file_type}"

                                
                                export_success = []
                                for i, part in enumerate(result, start=1):
                                    output_path = base_dir / f"{base_name}_{i:03d}{ext}"
                                    export_success.append(export_gdf(part, output_path))

                                
                                status_flags.append(all(export_success))

                            else:
                                
                                QMessageBox.information(self, "파일 오류", "파일 저장 중 오류가 발생했습니다.", QMessageBox.Ok)
                                status_flags.append(False)

                except Exception as e:
                    
                    QMessageBox.information(self, "파일 오류", f"GeoDataFrame export 실패: {e}", QMessageBox.Ok)
                    status_flags.append(False)

                
                current_step += 1
                update_progress(self.progressBar, int(base_progress + current_step * step_weight))

            
            if status_flags and all(status_flags):
                update_progress(self.progressBar, 100)  
                QMessageBox.information(self, "알림", "축하합니다. 작업이 완료했습니다!", QMessageBox.Ok)

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

        finally:
            show_progress(self.progressBar, False)

    
    
    

    
    def calculate_null_statistics(self, gdf):
        
        try:
            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf.copy()

            
            gdf_copy_no_geom = gdf_copy.drop(columns=geometry_col, errors='ignore')

            
            header, rows = ["필드 이름", "전체 개수", "Null 값 개수", "빈 값 개수", "결측값 비율 (%)"], []

            
            total = len(gdf_copy_no_geom)

            
            for col in gdf_copy_no_geom.columns:

                
                empty_flags = gdf_copy_no_geom[col].apply(is_empty_value)
                empty_count = empty_flags.sum()

                
                null_flags = gdf_copy_no_geom[col].apply(is_real_null)
                null_count = null_flags.sum()

                
                ratio = round((empty_count + null_count) / total * 100, 1)

                
                rows.append([col, total, null_count, empty_count, ratio])

            
            gdf_copy_not_null_values = normalize_null_values(gdf_copy_no_geom)

            
            gdf_copy_count_no_na = gdf_copy_not_null_values.dropna().shape[0]

            
            gdf_copy_count_na = total - gdf_copy_count_no_na

            
            ratio = round(gdf_copy_count_na / total * 100, 3)

            
            common.fileInfo_1.result_table["header"] = header
            common.fileInfo_1.result_table["rows"] = rows
            common.fileInfo_1.result_table["msg"] = (
                f"통계 결과: 총 레코드 수:  {total},  누락 (NULL/빈 값) 레코드 수:  {gdf_copy_count_na},  누락 비율:  {ratio}%"
            )

            
            common.signals.file_preview_updated.emit()

            return True

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "파일을 작업 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return False

    
    def replace_nulls_with_defaults(self, gdf, array_string="N/A", array_integer=0, array_float=0.0):
        
        def is_int_or_float(series: pd.Series) -> str:
            
            if pd.api.types.is_integer_dtype(series):
                return "int"
            elif pd.api.types.is_float_dtype(series):
                return "float"

            
            
            try:
                numeric_series = pd.to_numeric(series, errors='raise')

                
                s_no_na = numeric_series.dropna()
                if s_no_na.empty:
                    return "int"  

                return "int" if (s_no_na % 1 == 0).all() else "float"
            except:
                return "string"

        def fill_nulls(gdf):
            
            for col in gdf.columns:

                col_type = is_int_or_float(gdf[col])

                if col_type == "int":
                    gdf[col] = gdf[col].fillna(array_integer)  

                elif col_type == "float":
                    gdf[col] = gdf[col].fillna(array_float)  

                else:
                    gdf[col] = gdf[col].fillna(array_string)  

            return gdf  

        try:
            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf.copy()

            
            gdf_copy_no_geom = gdf_copy.drop(columns=geometry_col, errors='ignore')

            
            gdf_copy_not_null_values = normalize_null_values(gdf_copy_no_geom)

            
            gdf_copy_not_null_values = fill_nulls(gdf_copy_not_null_values)

            
            gdf_copy.update(gdf_copy_not_null_values)

            
            return gdf_copy

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "파일을 작업 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return None

    
    def remove_null_rows(self, gdf):
        
        try:
            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf.copy()

            
            gdf_copy_no_geom = gdf_copy.drop(columns=geometry_col, errors='ignore')

            
            gdf_copy_not_null_values = normalize_null_values(gdf_copy_no_geom)

            
            gdf_copy_not_null_values = gdf_copy_not_null_values.dropna(how='any')

            
            gdf_cleaned = gdf.loc[gdf_copy_not_null_values.index].copy()

            return gdf_cleaned

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "파일을 작업 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return None

    
    def select_null_features_from_gdf(self, layer: QgsVectorLayer, gdf):
        
        try:
            
            layer.removeSelection()

            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf.copy()

            
            gdf_copy_no_geom = gdf_copy.drop(columns=geometry_col, errors='ignore')

            
            gdf_copy_not_null_values = normalize_null_values(gdf_copy_no_geom)

            
            null_rows = gdf_copy_not_null_values.isna().any(axis=1)
            target_indexes = gdf_copy_not_null_values[null_rows].index.tolist()

            if not target_indexes:
                QMessageBox.information(
                    self,
                    "작업 결과",
                    "작업은 정상적으로 수행되었으나,\n\n"
                    "처리 조건에 해당하는 객체가 없어 변경 사항이 없습니다.\n\n"
                    "입력 데이터와 설정 값을 확인해 주세요.",
                    QMessageBox.Ok
                )
                return False

            
            target_fids = [f.id() for f in layer.getFeatures() if f.id() in target_indexes]

            
            layer.removeSelection()
            layer.selectByIds(target_fids)

            return True

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "파일을 작업 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return False

    
    def calculate_duplicate_statistics(self, gdf):
        
        try:
            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf.copy()

            
            gdf_copy_no_geom = gdf_copy.drop(columns=geometry_col, errors='ignore')

            
            gdf_copy_not_null_values = normalize_null_values(gdf_copy_no_geom)

            
            header = ["필드 이름", "전체 개수", "중복 값", "중복 횟수", "중복값 비율 (%)"]
            rows = []

            total_rows = len(gdf_copy_not_null_values)

            
            for col in gdf_copy_not_null_values.columns:
                series = gdf_copy_not_null_values[col]

                
                null_flags = series.isna() | series.astype(str).str.strip().eq("")
                valid_series = series[~null_flags]

                
                value_counts = valid_series.value_counts()
                duplicates = value_counts[value_counts > 1]

                for val, count in duplicates.items():
                    rows.append([
                        col,
                        total_rows,
                        str(val),
                        count,
                        round(count / total_rows * 100, 3)
                    ])

            
            common.fileInfo_1.result_table["header"] = header
            common.fileInfo_1.result_table["rows"] = rows
            common.fileInfo_1.result_table["msg"] = (
                f"통계 결과: 전체 {len(gdf_copy_not_null_values.columns)}개 필드 중 "
                f"중복이 발생한 값 총 {len(rows)}가지입니다."
            )

            
            common.signals.file_preview_updated.emit()

            return True

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "중복값 통계 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("중복값 통계 에러: %s", e, exc_info=True)
            return False

    
    def remove_duplicate_values_per_column(self, gdf, field_name):
        
        try:
            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf[[field_name]].copy()

            
            gdf_copy_no_geom = gdf_copy.drop(columns=geometry_col, errors='ignore')

            
            gdf_copy_not_null_values = normalize_null_values(gdf_copy_no_geom)

            
            duplicate_mask = gdf_copy_not_null_values[field_name].duplicated(keep='first')

            
            indexes_to_drop = gdf_copy_not_null_values[duplicate_mask].index

            
            gdf_result = gdf.drop(index=indexes_to_drop).reset_index(drop=True)

            return gdf_result

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "파일을 작업 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return None

    
    
    

    @staticmethod
    def get_widget_option(job_index, job_title):
        
        try:
            option = None  
            job_title = job_title[2:]

            if job_index == 0:
                option = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": True,
                    "disable_file_type_txtcsv": True,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": True,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": False, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": False,
                    "output_by_field": False,
                    "output_by_table": True,

                    "RESULT_TABLE": [
                        f'{job_title} 결과 테이블',
                        ''
                    ]
                }
            if job_index == 1:
                option = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": True,
                    "disable_file_type_txtcsv": True,
                    "disable_file_type_fold": True,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": True,

                    "setting_by_text": False,
                    "setting_by_array": True,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": False, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,
                    "output_by_field": False,
                    "output_by_table": False,

                    "SETTING_ARRAY": [
                        '필드 유형별 NULL/빈 값 채우기 설정',
                        '문자열(String) 필드 대체 값: ',
                        '정수(Integer) 필드 대체 값: ',
                        '실수(Float) 필드 대체 값: ',
                        '각 필드 유형의 NULL 또는 빈 값을 대체할 값을 입력해 주세요.'
                    ],
                }
            if job_index == 2:
                option = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": True,
                    "disable_file_type_txtcsv": True,
                    "disable_file_type_fold": True,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": True,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": False, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,
                    "output_by_field": False,
                    "output_by_table": False,
                }

            if job_index == 3:
                option = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": False,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": True,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": False, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": False,
                    "output_by_field": False,
                    "output_by_table": False,
                }
            if job_index == 4:
                option = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": True,
                    "disable_file_type_txtcsv": True,
                    "disable_file_type_fold": False,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": False,
                    "show_field_in_file": True,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": False, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": False,
                    "output_by_field": False,
                    "output_by_table": True,

                    "RESULT_TABLE": [
                        f'{job_title} 결과 테이블',
                        ''
                    ],
                }
            if job_index == 5:
                option = {
                    "apply_basic_qss": True,

                    "disable_file_type_layer": True,
                    "disable_file_type_shp": True,
                    "disable_file_type_json": True,
                    "disable_file_type_txtcsv": True,
                    "disable_file_type_fold": True,

                    "show_uid_in_file": False,
                    "show_tuid_in_file": True,
                    "show_field_in_file": True,

                    "setting_by_text": False,
                    "setting_by_array": False,
                    "setting_by_expression": False,
                    "setting_by_section": {"enabled": False, "value_type": "int"},
                    "setting_by_numeric": {"enabled": False, "value_type": "int"},
                    "setting_by_combo": {"enabled": False, "items": []},

                    "output_by_file": True,
                    "output_by_field": False,
                    "output_by_table": False,

                    "FILE_TUID": [
                        '중복 제거 기준 필드 선택',
                        '필드 선택: ',
                        '선택한 필드를 기준으로 중복된 값을 가진 행을 찾아, 하나를 제외한 나머지 중복 행을 삭제합니다.\n'
                        "예: 이름 필드에 '홍길동'이 3건 있을 경우, 첫 번째 행만 유지하고 나머지 두 행은 삭제됩니다."
                    ],
                }
            return option

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)

    def run_job_by_index(self, gdf, file_preview_index):
        
        try:
            
            file_info = common.fileInfo_1

            
            setting_text = file_info.file_setting.get_text()
            setting_numeric = file_info.file_setting.get_numeric()
            setting_section_min, setting_section_max = file_info.file_setting.get_section()
            setting_combo = file_info.file_setting.get_combo()
            setting_array_string, setting_array_integer, setting_array_float = file_info.file_setting.get_array()

            
            source_file_type, source_file_path, source_file_name = file_info.file_record.get_record()

            
            file_preview = file_info.file_preview[file_preview_index]
            file_field_selection = file_preview.get_selection_field()
            file_tuid = file_preview.get_file_tuid()
            file_is_field_check = file_preview.get_field_check()

            
            result = None
            if self.job_index == 0:
                gdf = keep_columns_gdf(gdf, file_field_selection) if file_is_field_check else gdf
                result = self.calculate_null_statistics(gdf)

            elif self.job_index == 1:
                gdf = keep_columns_gdf(gdf, file_field_selection) if file_is_field_check else gdf
                result = self.replace_nulls_with_defaults(gdf, setting_array_string, setting_array_integer, setting_array_float)

            elif self.job_index == 2:
                gdf = keep_columns_gdf(gdf, file_field_selection) if file_is_field_check else gdf
                result = self.remove_null_rows(gdf)

            elif self.job_index == 3:
                layer = source_file_path
                gdf = keep_columns_gdf(gdf, file_field_selection) if file_is_field_check else gdf
                result = self.select_null_features_from_gdf(layer, gdf)

            elif self.job_index == 4:
                gdf = keep_columns_gdf(gdf, file_field_selection) if file_is_field_check else gdf
                result = self.calculate_duplicate_statistics(gdf)

            elif self.job_index == 5:
                gdf = keep_columns_gdf(gdf, file_field_selection + [file_tuid]) if file_is_field_check else gdf
                result = self.remove_duplicate_values_per_column(gdf, file_tuid)

            
            if result is None or result is False:
                return None

            return result

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)
            return None




