
import os
import re
import hashlib
from pyproj import CRS
from pathlib import Path
from shapely.geometry import box


from qgis.core import QgsVectorLayer


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


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.widgetutils import (
    show_progress,
    update_progress,
)

from urbanq.function.geo import (
    normalize_null_values,
)


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__), 'GeometryValidation_dockwidget_base.ui'))


class GeometryValidationDockWidget(QDialog, FORM_CLASS):  
    def __init__(self, parent=None):
        
        super(GeometryValidationDockWidget, 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.spatialAnalysis.spatialAnalysis_dockwidget import spatialAnalysisDockWidget  
            parent_ui = spatialAnalysisDockWidget(self)  
            main_page_layout = self.parent().parent().findChild(QWidget, "page_spatialAnalysis").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 auto_fix_korea_coordinates_by_overlap(self, gdf):
        
        
        
        
        
        
        
        
        
        
        
        
        def make_korea_boundary_polygon_bbox():
            
            
            korea_poly = box(123.0, 32.0, 132.5, 39.5)  

            korea = gpd.GeoDataFrame({"name": ["Korea_bbox"]}, geometry=[korea_poly], crs="EPSG:4326")
            return korea, korea.geometry.iloc[0]

        KOREA_CRS_WKT = {

            
            4326: 'GEOGCS["WGS 84",DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]]',

            
            5179: 'PROJCS["Korea 2000 / Unified CS",GEOGCS["Korea 2000",'
                  'DATUM["Korean_Datum_2002",SPHEROID["GRS 1980",6378137,298.257222101]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",127.5],PARAMETER["scale_factor",0.9996],'
                  'PARAMETER["false_easting",1000000],PARAMETER["false_northing",2000000],'
                  'UNIT["metre",1]]',

            
            5181: 'PROJCS["Korea 2000 / West Belt",GEOGCS["Korea 2000",'
                  'DATUM["Korean_Datum_2002",SPHEROID["GRS 1980",6378137,298.257222101]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",125],PARAMETER["scale_factor",1],'
                  'PARAMETER["false_easting",200000],PARAMETER["false_northing",500000],'
                  'UNIT["metre",1]]',

            
            5185: 'PROJCS["Korea 2000 / Central Belt",GEOGCS["Korea 2000",'
                  'DATUM["Korean_Datum_2002",SPHEROID["GRS 1980",6378137,298.257222101]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",127],PARAMETER["scale_factor",1],'
                  'PARAMETER["false_easting",200000],PARAMETER["false_northing",500000],'
                  'UNIT["metre",1]]',

            
            5186: 'PROJCS["Korea 2000 / East Belt",GEOGCS["Korea 2000",'
                  'DATUM["Korean_Datum_2002",SPHEROID["GRS 1980",6378137,298.257222101]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",129],PARAMETER["scale_factor",1],'
                  'PARAMETER["false_easting",200000],PARAMETER["false_northing",500000],'
                  'UNIT["metre",1]]',

            
            5187: 'PROJCS["Korea 2000 / East Sea Belt",GEOGCS["Korea 2000",'
                  'DATUM["Korean_Datum_2002",SPHEROID["GRS 1980",6378137,298.257222101]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",131],PARAMETER["scale_factor",1],'
                  'PARAMETER["false_easting",200000],PARAMETER["false_northing",500000],'
                  'UNIT["metre",1]]',

            
            2096: 'PROJCS["Korea 1985 / West Belt",GEOGCS["Korea 1985",'
                  'DATUM["Korean_Datum_1985",SPHEROID["Bessel 1841",6377397.155,299.1528128]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",125],PARAMETER["scale_factor",1],'
                  'PARAMETER["false_easting",200000],PARAMETER["false_northing",500000],'
                  'UNIT["metre",1]]',

            
            2097: 'PROJCS["Korea 1985 / Central Belt",GEOGCS["Korea 1985",'
                  'DATUM["Korean_Datum_1985",SPHEROID["Bessel 1841",6377397.155,299.1528128]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",127],PARAMETER["scale_factor",1],'
                  'PARAMETER["false_easting",200000],PARAMETER["false_northing",500000],'
                  'UNIT["metre",1]]',

            
            2098: 'PROJCS["Korea 1985 / East Belt",GEOGCS["Korea 1985",'
                  'DATUM["Korean_Datum_1985",SPHEROID["Bessel 1841",6377397.155,299.1528128]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",129],PARAMETER["scale_factor",1],'
                  'PARAMETER["false_easting",200000],PARAMETER["false_northing",500000],'
                  'UNIT["metre",1]]',

            
            2099: 'PROJCS["Korea 1985 / East Sea Belt",GEOGCS["Korea 1985",'
                  'DATUM["Korean_Datum_1985",SPHEROID["Bessel 1841",6377397.155,299.1528128]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",38],'
                  'PARAMETER["central_meridian",131],PARAMETER["scale_factor",1],'
                  'PARAMETER["false_easting",200000],PARAMETER["false_northing",500000],'
                  'UNIT["metre",1]]',

            
            32651: 'PROJCS["WGS 84 / UTM zone 51N",GEOGCS["WGS 84",'
                   'DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],'
                   'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                   'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],'
                   'PARAMETER["central_meridian",123],PARAMETER["scale_factor",0.9996],'
                   'PARAMETER["false_easting",500000],PARAMETER["false_northing",0],'
                   'UNIT["metre",1]]',

            
            32652: 'PROJCS["WGS 84 / UTM zone 52N",GEOGCS["WGS 84",'
                   'DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],'
                   'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                   'PROJECTION["Transverse_Mercator"],PARAMETER["latitude_of_origin",0],'
                   'PARAMETER["central_meridian",129],PARAMETER["scale_factor",0.9996],'
                   'PARAMETER["false_easting",500000],PARAMETER["false_northing",0],'
                   'UNIT["metre",1]]',

            
            3857: 'PROJCS["WGS 84 / Pseudo-Mercator",GEOGCS["WGS 84",'
                  'DATUM["WGS_1984",SPHEROID["WGS 84",6378137,298.257223563]],'
                  'PRIMEM["Greenwich",0],UNIT["degree",0.0174532925199433]],'
                  'PROJECTION["Mercator_1SP"],PARAMETER["central_meridian",0],'
                  'PARAMETER["scale_factor",1],PARAMETER["false_easting",0],'
                  'PARAMETER["false_northing",0],UNIT["metre",1]]'
        }

        metric_epsg = 5179

        
        korea_gdf, korea_poly4326 = make_korea_boundary_polygon_bbox()
        korea_poly_metric = gpd.GeoSeries(
            [korea_poly4326], crs="EPSG:4326"
        ).to_crs(epsg=metric_epsg).iloc[0]

        best_epsg = None
        best_score = -1.0

        
        for epsg in KOREA_CRS_WKT.keys():
            try:
                
                
                
                tmp = gdf.copy()
                tmp = tmp.set_crs(CRS.from_wkt(KOREA_CRS_WKT[epsg]), allow_override=True)

                
                
                
                
                tmp = tmp.to_crs(epsg=metric_epsg)

                
                
                
                
                try:
                    tmp["geometry"] = tmp.geometry.make_valid()
                except Exception:
                    
                    tmp["geometry"] = tmp.geometry.buffer(0)

                
                tmp = tmp[~tmp.geometry.is_empty & tmp.geometry.notna()]

                
                
                
                
                inter_geoms = tmp.geometry.intersection(korea_poly_metric)

                
                gtype = tmp.geometry.geom_type.iloc[0]  

                if gtype in ("Polygon", "MultiPolygon"):
                    score = float(inter_geoms.area.sum())

                elif gtype in ("LineString", "MultiLineString"):
                    score = float(inter_geoms.length.sum())

                elif gtype in ("Point", "MultiPoint"):
                    
                    score = float(tmp.geometry.within(korea_poly_metric).sum())

                else:
                    
                    score = float(tmp.geometry.within(korea_poly_metric).sum())

                
                
                
                if score > best_score:
                    best_score = score
                    best_epsg = epsg

            except Exception:
                
                continue

        
        
        
        if best_epsg is None or best_score <= 0:
            QMessageBox.information(
                self,
                "좌표계 자동 보정 실패",
                "파일의 좌표계를 보정하지 못했습니다.",
                QMessageBox.Ok
            )
            return None

        
        
        
        
        
        try:
            gdf = gdf.set_crs(CRS.from_wkt(KOREA_CRS_WKT[best_epsg]), allow_override=True)
            gdf = gdf.to_crs(epsg=metric_epsg)

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)
            QMessageBox.information(
                self,
                "좌표계 자동 보정 실패",
                f"대한민국 위치 보정 중 좌표계 추정 과정에서 오류가 발생하였습니다.\n\n"
                f"파일의 좌표계 정보가 손상되었거나 지원하지 않는 좌표계일 수 있습니다.\n\n"
                f"오류 내용:\n{e}",
                QMessageBox.Ok
            )

        return gdf

    def get_geometry_error_reason(self, geom):
        
        try:
            
            if geom is None:
                return "지오메트리 없음"

            
            if isinstance(geom, float) and np.isnan(geom):
                return "결측값(NaN)"

            
            if not isinstance(geom, BaseGeometry):
                return "형식 오류(비정상 객체)"

            
            if geom.is_empty:
                return "비어 있음(Empty Geometry)"

            
            if not geom.is_valid:
                return "유효하지 않음(Invalid Geometry)"

            
            if geom.geom_type.startswith("Multi"):
                return "다중 도형(MultiGeometry)"

            return None  

        except Exception as e:
            logger.error("에러 발생: %s", e, exc_info=True)
            return "예외 발생(검사 실패)"

    
    def calculate_geometry_error_statistics(self, gdf):
        
        try:
            total_features = len(gdf)

            
            error_reasons = gdf["geometry"].apply(self.get_geometry_error_reason)

            
            invalid_mask = error_reasons.notna()
            invalid_gdf = gdf[invalid_mask].copy()
            invalid_count = len(invalid_gdf)

            if invalid_count == 0:
                QMessageBox.information(self, "알림", "지오메트리가 모두 유효합니다.", QMessageBox.Ok)
                return False

            
            error_ratio = round((invalid_count / total_features) * 100, 1) if total_features > 0 else 0.0

            
            invalid_gdf.insert(0, "오류 사항", error_reasons[invalid_mask])

            
            msg = (
                f"통계 결과: 총 객체 수 {total_features}개 중 오류 객체는 {invalid_count}개이며, "
                f"오류율은 {error_ratio}%입니다. 아래 표를 참고해 주세요."
            )

            
            header = list(invalid_gdf.columns)
            rows = invalid_gdf.values.tolist()

            
            common.fileInfo_1.result_table["header"] = header
            common.fileInfo_1.result_table["rows"] = rows
            common.fileInfo_1.result_table["msg"] = msg

            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 select_invalid_geometry_features(self, layer: QgsVectorLayer, gdf):
        
        try:
            if not isinstance(layer, QgsVectorLayer):
                QMessageBox.warning(self, "레이어 오류", "올바른 레이어 또는 데이터를 불러오지 못했습니다.", QMessageBox.Ok)
                return False

            
            layer.removeSelection()

            
            error_reasons = gdf["geometry"].apply(self.get_geometry_error_reason)

            
            invalid_indexes = error_reasons[error_reasons.notna()].index.tolist()

            if not invalid_indexes:
                QMessageBox.information(self, "알림", "지오메트리 모두 유효합니다.", QMessageBox.Ok)
                return False

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

            
            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 remove_duplicate_geometries(self, gdf):
        
        try:
            
            gdf_copy = gdf.copy()

            
            geom_col = gdf_copy.geometry.name

            
            gdf_copy["_geom_wkb"] = gdf_copy[geom_col].apply(
                lambda g: g.wkb if g is not None else None
            )

            
            before = len(gdf_copy)
            gdf_copy = gdf_copy.drop_duplicates(subset=["_geom_wkb"], keep="first")
            after = len(gdf_copy)

            
            gdf_copy = gdf_copy.drop(columns=["_geom_wkb"])

            
            if before == after:
                QMessageBox.information(self, "결과", "중복된 지오메트리 객체가 없습니다.", QMessageBox.Ok)

            return gdf_copy.copy()

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

    
    def remove_duplicate_features(self, gdf, field_list):
        
        try:
            gdf_copy = gdf.copy()
            geom_col = gdf_copy.geometry.name

            
            
            
            SENTINEL = "__<NA>__"

            def geom_hash(geom):
                if geom is None:
                    return SENTINEL
                wkb = geom.wkb  
                
                return hashlib.sha1(wkb).hexdigest()

            gdf_copy["_geom_hash"] = gdf_copy[geom_col].apply(geom_hash)

            
            key_cols = ["_geom_hash"] + field_list

            for f in field_list:
                gdf_copy[f] = gdf_copy[f].astype("object").fillna(SENTINEL).astype(str)

            
            
            
            before = len(gdf_copy)
            gdf_copy = gdf_copy.drop_duplicates(subset=key_cols, keep="first")
            after = len(gdf_copy)

            
            gdf_copy = gdf_copy.drop(columns=["_geom_hash"])

            if before == after:
                QMessageBox.information(self, "결과", "중복된 객체가 없습니다.", QMessageBox.Ok)

            return gdf_copy.copy()

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

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

            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf.copy()

            
            gdf_copy = gdf_copy[~gdf_copy[geometry_col].isna()].copy()
            gdf_copy = gdf_copy[~gdf_copy[geometry_col].is_empty].copy()

            if gdf_copy.empty:
                return False

            
            try:
                sindex = gdf_copy.sindex
            except Exception as e:
                QMessageBox.information(self, "작업 오류", "공간 인덱스 생성에 실패했습니다.", QMessageBox.Ok)
                logger.error("에러 발생: %s", e, exc_info=True)
                return False

            
            overlapped_indexes = set()
            idx_list = list(gdf_copy.index)  

            for i_pos, i in enumerate(idx_list):
                geom_i = gdf_copy.at[i, geometry_col]

                
                candidates_pos = list(sindex.intersection(geom_i.bounds))

                for j_pos in candidates_pos:
                    if j_pos <= i_pos:
                        continue  

                    j = idx_list[j_pos]
                    geom_j = gdf_copy.at[j, geometry_col]

                    
                    if not geom_i.intersects(geom_j):
                        continue
                    if geom_i.touches(geom_j):
                        continue

                    
                    try:
                        inter = geom_i.intersection(geom_j)
                        if (not inter.is_empty) and getattr(inter, "area", 0) > 0:
                            overlapped_indexes.add(i)
                            overlapped_indexes.add(j)
                    except Exception:
                        
                        continue

            target_indexes = sorted(overlapped_indexes)

            if not target_indexes:
                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 select_sliver_polygons_from_gdf(self, layer: QgsVectorLayer, gdf, area):
        try:
            if gdf.crs is None or str(gdf.crs).strip() == "":
                QMessageBox.information(
                    self, "좌표계 안내",
                    "이 레이어에는 좌표계(CRS)가 없습니다.\n"
                    "슬리버 폴리곤 검출은 면적 계산이 필요하므로\n"
                    "CRS를 먼저 지정한 후 다시 실행해 주세요.",
                    QMessageBox.Ok
                )
                return False

            layer.removeSelection()
            geometry_col = gdf.geometry.name
            gdf_copy = gdf.copy()

            
            gdf_copy = gdf_copy[~gdf_copy[geometry_col].isna()]
            gdf_copy = gdf_copy[~gdf_copy[geometry_col].is_empty]

            if gdf_copy.empty:
                return False

            
            crs = CRS.from_user_input(gdf_copy.crs)
            if crs.is_geographic:
                gdf_metric = gdf_copy.to_crs(epsg=5179)
            else:
                gdf_metric = gdf_copy

            
            areas = gdf_metric[geometry_col].area

            sliver_rows = areas < float(area)
            target_indexes = gdf_metric[sliver_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

    
    
    

    @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": False,
                    "disable_file_type_shp": True,
                    "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": False,

                    "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 == 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": 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 == 2:
                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": False,

                    "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 == 3:
                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": False,

                    "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 == 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": 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,

                    "FILE_FIELD": [
                        '중복 확인 기준 필드 선택',
                        '필드 선택: ',
                        '지오메트리와 선택한 필드 값이 모두 동일한 경우 중복으로 판단합니다.'
                    ],
                }
            if job_index == 5:
                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": False,

                    "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 == 6:
                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": False,

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

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

                    "SETTING_NUMERIC": [
                        '슬리버 검출 면적 기준값 설정 (㎡)',
                        '면적 기준값 입력 (㎡): ',
                        '입력한 면적 기준값(㎡)보다 작은 폴리곤을 슬리버로 판단하여 지도에 표시합니다.'
                    ],
                }
            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_uid = file_preview.get_file_uid()
            file_tuid = file_preview.get_file_tuid()
            file_is_field_check = file_preview.get_field_check()
            result_field = file_info.result_field

            
            gdf.columns = gdf.columns.astype(str)

            
            result = None
            if self.job_index == 0:
                result = self.auto_fix_korea_coordinates_by_overlap(gdf)

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

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

            elif self.job_index == 3:
                result = self.remove_duplicate_geometries(gdf)

            elif self.job_index == 4:
                geom_col = gdf.geometry.name
                if not file_field_selection:
                    file_field_selection = [c for c in gdf.columns if c != geom_col]
                else:
                    file_field_selection = list(file_field_selection)
                result = self.remove_duplicate_features(gdf, file_field_selection)

            elif self.job_index == 5:
                layer = source_file_path
                result = self.select_overlap_features_from_gdf(layer, gdf)

            elif self.job_index == 6:
                layer = source_file_path
                result = self.select_sliver_polygons_from_gdf(layer, gdf, setting_numeric)

            
            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



