
import os
import re
from pathlib import Path


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


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,
)

import pandas as pd
import geopandas as gpd
from typing import Optional

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


class DataManagementDockWidget(QDialog, FORM_CLASS):  
    def __init__(self, parent=None):
        
        super(DataManagementDockWidget, 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_1, self.option_2 = 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_1, None))

            
            pages.append((True, self.current_step_3, fileRreadDockWidget, self.option_2, None))

            
            save_required = any([
                self.option_1["output_by_file"],
                self.option_1["output_by_field"],
                self.option_1["output_by_table"]
            ])
            pages.append((save_required, self.current_step_4, fileSaveDockWidget, self.option_1, 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)

            option = self.option_1 if index == 1 else self.option_2

            
            if widget_instance is None:

                
                widget_instance = widget_class(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(1 if current_index != 2 else 2):
                    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_2.file_preview) + 1  
            base_progress = 20  
            step_weight = (100 - base_progress) / total_files  
            current_step = 0  

            
            fileinfo = common.fileInfo_1
            file_preview_index = 0
            file_preview = fileinfo.file_preview[file_preview_index]

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

            
            file_field_selection = file_preview.get_selection_field()
            file_uid = file_preview.get_file_uid()
            file_is_field_check = file_preview.get_field_check()
            file_path, file_encoding, file_delimiter, file_has_header = file_preview.get_info()

            
            new_file_path = file_path if result_file_type == "layer" else 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(current_step * step_weight))

            
            status_flags = []
            result = self.run_job_by_index(gdf, file_uid, file_is_field_check, file_field_selection, current_step, step_weight)

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

            if result is not None:
                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 fast_gdf_join(self,
                      gdf1: gpd.GeoDataFrame,
                      gdf2: gpd.GeoDataFrame,
                      join_id1: str,
                      join_id2: str,
                      how: str = 'left',  
                      validate: Optional[str] = None,
                      suffix: str = '_right'
                      ) -> Optional[gpd.GeoDataFrame]:
        

        
        if validate is None:  
            if not gdf1[join_id1].is_unique:
                QMessageBox.information(self, "ID 중복 경고",
                                        f"첫 번째 데이터의 조인 키 필드 '{join_id1}'에 중복 값이 발견되었습니다.\n"
                                        f"※ 정확한 조인을 위해서는 조인 키 컬럼 값이 모두 유일해야 합니다.", QMessageBox.Ok)
                return None

            if not gdf2[join_id2].is_unique:
                QMessageBox.information(self, "ID 중복 경고",
                                        f"두 번째 데이터의 조인 키 필드 '{join_id2}'에 중복 값이 발견되었습니다.\n"
                                        f"※ 정확한 조인을 위해서는 조인 키 컬럼 값이 모두 유일해야 합니다.", QMessageBox.Ok)
                return None

        
        gdf2_copy = gdf2.drop(columns=[gdf2.geometry.name], errors='ignore').copy()

        
        common_columns = set(gdf1.columns).intersection(set(gdf2_copy.columns)) - {join_id1, join_id2}
        if common_columns:
            QMessageBox.information(self, "작업 오류",
                                    f"다음 컬럼 이름이 양쪽 데이터에서 중복됩니다:\n{common_columns}\n"
                                    "※ 이름 변경 후 다시 시도해주세요.", QMessageBox.Ok)
            return None

        
        cols1 = [join_id1] + [col for col in gdf1.columns if col != join_id1]
        cols2 = [join_id2] + [col for col in gdf2_copy.columns if col != join_id2]

        
        try:
            merged = pd.merge(
                gdf1[cols1],
                gdf2_copy[cols2],
                left_on=join_id1,
                right_on=join_id2,
                how=how,  
                suffixes=('', suffix),
                validate=validate
            )

            
            if join_id2 in merged.columns and join_id1 != join_id2:
                merged.drop(columns=[join_id2], inplace=True)

        except pd.errors.MergeError as e:
            QMessageBox.critical(self, "조인 오류", f"조인 과정에서 오류 발생: {str(e)}", QMessageBox.Ok)
            return None

        
        if len(merged) == 0:
            QMessageBox.warning(self, "결과 없음", "조인 결과가 비어 있습니다.", QMessageBox.Ok)
            return None

        
        result_gdf = gpd.GeoDataFrame(merged, geometry=gdf1.geometry.name, crs=gdf1.crs)

        return result_gdf

    
    def fast_attribute_sync(
            self,
            gdf1: gpd.GeoDataFrame,  
            gdf2: gpd.GeoDataFrame,  
            join_id1: str,  
            join_id2: str,  
            how: str = 'left',  
            validate: Optional[str] = None  
    ) -> Optional[gpd.GeoDataFrame]:
        

        try:
            
            if validate is None:  
                if not gdf1[join_id1].is_unique:
                    QMessageBox.information(self, "ID 중복 경고",
                                            f"첫 번째 데이터의 조인 키 필드 '{join_id1}'에 중복 값이 발견되었습니다.\n"
                                            f"※ 정확한 통기화를 위해서는 조인 키 컬럼 값이 모두 유일해야 합니다.", QMessageBox.Ok)
                    return None

                if not gdf2[join_id2].is_unique:
                    QMessageBox.information(self, "ID 중복 경고",
                                            f"두 번째 데이터의 조인 키 필드 '{join_id2}'에 중복 값이 발견되었습니다.\n"
                                            f"※ 정확한 통기화를 위해서는 조인 키 컬럼 값이 모두 유일해야 합니다.", QMessageBox.Ok)
                    return None

            
            gdf2_copy = gdf2.drop(columns=[gdf2.geometry.name], errors='ignore')

            
            common_fields = [
                col for col in gdf2_copy.columns
                if col != join_id2 and col in gdf1.columns
            ]

            if not common_fields:
                QMessageBox.warning(self, "업데이트 불가", "공통된 속성 필드가 없습니다.", QMessageBox.Ok)
                return None

            
            gdf1_backup_geometry = gdf1.geometry  
            merged = gdf1.merge(
                gdf2_copy[[join_id2] + common_fields],
                how=how,
                left_on=join_id1,
                right_on=join_id2,
                validate=validate,
                suffixes=('', '_update')
            )

            
            merged.set_geometry(gdf1.geometry.name, inplace=True)
            merged[gdf1.geometry.name] = gdf1.geometry

            
            for col in common_fields:
                update_col = col + "_update"
                if update_col in merged.columns:
                    merged[col] = merged[update_col].combine_first(merged[col])
                    merged.drop(columns=[update_col], inplace=True)

            
            result_gdf = gpd.GeoDataFrame(merged, geometry=gdf1.geometry.name, crs=gdf1.crs)

            return result_gdf

        except Exception as e:
            QMessageBox.critical(self, "동기화 오류", f"속성 동기화 중 오류 발생:\n{str(e)}", QMessageBox.Ok)
            return None

    
    
    

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

            if job_index == 0:
                option_1 = {
                    "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": True,
                    "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,
                }
                option_2 = {
                    "apply_basic_qss": False,

                    "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": True,
                    "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 == 1:
                option_1 = {
                    "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": True,
                    "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,
                }
                option_2 = {
                    "apply_basic_qss": False,

                    "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": True,
                    "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,
                }

            return option_1, option_2

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

    def run_job_by_index(self, gdf_1, file_uid_1, file_is_field_check_1, file_field_selection_1, current_step, step_weight):
        
        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()

            
            file_info_2 = common.fileInfo_2

            
            source_file_type, source_file_path, _ = file_info_2.file_record.get_record()

            for index, file_preview in enumerate(common.fileInfo_2.file_preview):

                
                file_preview = file_info_2.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()
                file_path, file_encoding, file_delimiter, file_has_header = file_preview.get_info()

                
                gdf_2 = self.get_file_data_frame(source_file_type, source_file_path, file_path, file_encoding, file_delimiter, file_has_header)

                
                gdf_1.columns = gdf_1.columns.astype(str)
                gdf_2.columns = gdf_2.columns.astype(str)

                if self.job_index == 0:
                    gdf_1 = keep_columns_gdf(gdf_1, file_field_selection_1 + [file_uid_1]) if file_is_field_check_1 else gdf_1
                    gdf_2 = keep_columns_gdf(gdf_2, file_field_selection + [file_uid]) if file_is_field_check else gdf_2
                    gdf_1 = self.fast_gdf_join(gdf_1, gdf_2, file_uid_1, file_uid)

                elif self.job_index == 1:
                    gdf_1 = keep_columns_gdf(gdf_1, file_field_selection_1 + [file_uid_1]) if file_is_field_check_1 else gdf_1
                    gdf_2 = keep_columns_gdf(gdf_2, file_field_selection + [file_uid]) if file_is_field_check else gdf_2
                    gdf_1 = self.fast_attribute_sync(gdf_1, gdf_2, file_uid_1, file_uid)

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

            
            if gdf_1 is None or gdf_1 is False:
                return None

            return gdf_1

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