
import os
import re
from pathlib import Path


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 typing import Optional


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


class DataAttachingDockWidget(QDialog, FORM_CLASS):  
    def __init__(self, parent=None):
        
        super(DataAttachingDockWidget, 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 = 5  
            total_steps = total_files * steps_per_file  
            base_progress = 0  
            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()

            
            result = None

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

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

                
                result = self.run_job_by_index(result, 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)

                
                status_flags.append(True)

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

            
            if status_flags and all(status_flags):

                
                if result_file_path and isinstance(result, gpd.GeoDataFrame):
                    export_gdf(result, result_file_path)

                
                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_vertical_concat_by_fields(self, gdf1: gpd.GeoDataFrame, gdf2: gpd.GeoDataFrame) -> Optional[gpd.GeoDataFrame]:
        

        try:
            
            if gdf1.shape[1] != gdf2.shape[1]:
                QMessageBox.warning(self, "열 개수 불일치", "두 데이터의 열 개수가 다릅니다. 병합할 수 없습니다.", QMessageBox.Ok)
                return None

            
            def has_valid_geometry(gdf):
                return 'geometry' in gdf.columns and not gdf.geometry.is_empty.all()

            
            if has_valid_geometry(gdf1) and has_valid_geometry(gdf2):
                if gdf1.geom_type.unique()[0] != gdf2.geom_type.unique()[0]:
                    QMessageBox.warning(self, "Geometry 유형 불일치", "두 GeoDataFrame의 geometry 유형이 다릅니다.", QMessageBox.Ok)
                    return None

            
            def to_string_gdf(gdf):
                return gdf.drop(columns=['geometry'], errors='ignore').astype(str)

            
            gdf1_str = to_string_gdf(gdf1)
            gdf2_str = to_string_gdf(gdf2)

            
            result_gdf = pd.concat([gdf1_str, gdf2_str], ignore_index=True)

            
            geom_name = gdf1.geometry.name
            result_gdf[geom_name] = pd.concat([gdf1.geometry, gdf2.geometry], ignore_index=True)

            
            result_gdf = gpd.GeoDataFrame(result_gdf, geometry=geom_name, crs=gdf1.crs)

            return result_gdf

        except Exception as e:
            QMessageBox.critical(self, "병합 오류", f"GeoDataFrame 병합 중 오류 발생:\n{str(e)}", QMessageBox.Ok)
            return None

    
    def fast_horizontal_concat_by_rows(self, gdf1: gpd.GeoDataFrame, gdf2: gpd.GeoDataFrame) -> Optional[gpd.GeoDataFrame]:
        
        try:
            
            if gdf1.shape[0] != gdf2.shape[0]:
                QMessageBox.warning(self, "행 개수 불일치", "두 데이터의 행 개수가 다릅니다. 병합할 수 없습니다.", QMessageBox.Ok)
                return None

            
            def has_valid_geometry(gdf):
                return 'geometry' in gdf.columns and not gdf.geometry.is_empty.all()

            
            if has_valid_geometry(gdf1) and has_valid_geometry(gdf2):
                if gdf1.geom_type.unique()[0] != gdf2.geom_type.unique()[0]:
                    QMessageBox.warning(self, "Geometry 유형 불일치", "두 GeoDataFrame의 geometry 유형이 다릅니다.", QMessageBox.Ok)
                    return None

            
            def to_string_gdf(gdf):
                return gdf.drop(columns=['geometry'], errors='ignore').astype(str)

            
            gdf1_str = to_string_gdf(gdf1)
            gdf2_str = to_string_gdf(gdf2)

            
            gdf1_cols = set(map(str, gdf1_str.columns))
            new_cols = []
            used_names = gdf1_cols.copy()

            for i, col in enumerate(gdf2_str.columns):
                base = str(col)[:10]
                new_col = base
                suffix = 1
                while new_col in used_names or new_col in new_cols:
                    suffix_str = f"_{suffix}"
                    max_len = 10 - len(suffix_str)
                    new_col = base[:max_len] + suffix_str
                    suffix += 1
                    if suffix > 999:
                        raise ValueError(f"필드명 '{col}'에 대해 중복 회피 실패: 너무 많은 충돌 발생")
                new_cols.append(new_col)
                used_names.add(new_col)

            gdf2_str.columns = new_cols

            
            max_len = max(len(gdf1_str), len(gdf2_str))
            gdf1_str = gdf1_str.reindex(range(max_len))
            gdf2_str = gdf2_str.reindex(range(max_len))

            
            merged_df = pd.concat([gdf1_str, gdf2_str], axis=1)

            
            geometry_series = gdf1.geometry.reindex(range(max_len))
            merged_gdf = gpd.GeoDataFrame(merged_df, geometry=geometry_series, crs=gdf1.crs)

            return merged_gdf

        except Exception as e:
            QMessageBox.critical(self, "병합 오류", f"GeoDataFrame 병합 중 오류 발생:\n{str(e)}", QMessageBox.Ok)
            return None

    
    
    

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

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

                    "disable_file_type_layer": False,
                    "disable_file_type_shp": False,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "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,

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

                    "disable_file_type_layer": False,
                    "disable_file_type_shp": False,
                    "disable_file_type_json": False,
                    "disable_file_type_txtcsv": False,
                    "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,

                    "fold_to_file": True,
                }

            return option

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

    def run_job_by_index(self, gdf_1, file_preview_index):
        
        try:
            
            fileinfo_1 = common.fileInfo_1
            file_preview = fileinfo_1.file_preview[file_preview_index]

            
            source_file_type, source_file_path, _ = fileinfo_1.file_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()

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

            
            if gdf_1 is None:
                return gdf_2

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

            
            result = None
            if self.job_index == 2:
                result = self.fast_vertical_concat_by_fields(gdf_1, gdf_2)

            elif self.job_index == 3:
                result = self.fast_horizontal_concat_by_rows(gdf_1, gdf_2)

            
            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



