
import os
import re
import random
from pathlib import Path


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


from qgis.core import QgsVectorLayer


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


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


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

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

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


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



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


class DistanceCalculationDockWidget(QDialog, FORM_CLASS):  
    def __init__(self, parent=None):
        
        super(DistanceCalculationDockWidget, 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 calculate_same_layer_nearest_n_dist_and_uid(
            self,
            gdf: gpd.GeoDataFrame,
            n: int,
            uid: str,
            metric_epsg: int = 5179,
            dist_prefix: str = "min_dist_",
            uid_prefix: str = "min_uid_",
    ):
        
        try:
            
            if gdf is None or len(gdf) == 0:
                QMessageBox.information(self, "결과", "입력 레이어(gdf)에 레코드가 없습니다.", QMessageBox.Ok)
                return None

            if uid not in gdf.columns:
                QMessageBox.information(self, "입력 오류", f"UID 필드 '{uid}'가 레이어에 존재하지 않습니다.", QMessageBox.Ok)
                return None

            
            if gdf.crs is None:
                QMessageBox.information(self, "좌표계 오류", "레이어에 CRS가 없습니다.", QMessageBox.Ok)
                return None

            
            try:
                n = int(n)
                if n <= 0:
                    raise ValueError
            except Exception:
                QMessageBox.information(self, "입력 오류", "N 값은 1 이상의 정수여야 합니다.", QMessageBox.Ok)
                return None

            
            g_valid = gdf[~gdf.geometry.isna() & ~gdf.geometry.is_empty].copy()
            if len(g_valid) < 2:
                QMessageBox.information(self, "결과", "최근접 이웃 계산을 위한 객체가 충분하지 않습니다.", QMessageBox.Ok)
                return None

            
            crs = CRS.from_user_input(g_valid.crs)
            if crs.is_geographic:
                gm = g_valid.to_crs(epsg=metric_epsg)
            else:
                gm = g_valid

            
            n = min(n, len(gm) - 1)

            
            out = gdf.copy()
            for i in range(1, n + 1):
                out[f"{dist_prefix}{i}"] = np.nan
                out[f"{uid_prefix}{i}"] = None

            
            gm = gm.reset_index(drop=False)
            geom_list = list(gm.geometry)
            uid_list = list(gm[uid])
            index_list = list(gm["index"])

            
            if len(gm) ** 2 >= 200000:
                reply = QMessageBox.question(
                    self, "작업 안내",
                    "객체 수가 많아 최근접 계산에 시간이 오래 걸릴 수 있습니다.\n"
                    f"객체 수: {len(gm)} / N: {n}\n계속 진행하시겠습니까?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No
                )
                if reply == QMessageBox.No:
                    return None

            
            for i, src_geom in enumerate(geom_list):
                dists = np.array([src_geom.distance(g) for g in geom_list])
                dists[i] = np.inf  

                nearest = np.argsort(dists)[:n]

                for rank, j in enumerate(nearest, start=1):
                    out.loc[index_list[i], f"{dist_prefix}{rank}"] = float(dists[j])
                    out.loc[index_list[i], f"{uid_prefix}{rank}"] = uid_list[j]

            return out

        except Exception as e:
            QMessageBox.information(self, "작업 오류", "동일 레이어 최근접 계산 중 오류가 발생하였습니다.", QMessageBox.Ok)
            logger.error("에러 발생: %s", e, exc_info=True)
            return None

    
    def classify_distance_by_bins(
            self,
            gdf: gpd.GeoDataFrame,
            tuid: str,
            setting_text: str,
            setting_by_combo: str,
            result_field: str,
    ):
        

        def _parse_breaks(text: str):
            
            if text is None:
                return None

            s = str(text).strip()
            if not s:
                return None

            
            s = s.replace(";", ",").replace("\n", ",").replace("\t", ",").replace(" ", "")

            
            parts = [p for p in s.split(",") if p != ""]
            if len(parts) < 2:
                return None

            vals = []
            for p in parts:
                try:
                    vals.append(float(p))
                except Exception:
                    return None  

            vals = sorted(set(vals))
            if len(vals) < 2:
                return None

            return vals

        def _alpha_label(i: int) -> str:
            
            s = ""
            n = i
            while n > 0:
                n, r = divmod(n - 1, 26)
                s = chr(65 + r) + s
            return s

        try:
            import numpy as _np
            import pandas as _pd

            
            if gdf is None or len(gdf) == 0:
                QMessageBox.information(self, "결과", "입력 레이어(gdf)에 레코드가 없습니다.", QMessageBox.Ok)
                return None

            if not tuid or tuid not in gdf.columns:
                QMessageBox.information(self, "입력 오류", f"거리 필드(tuid) '{tuid}'가 레이어에 없습니다.", QMessageBox.Ok)
                return None

            if not result_field or str(result_field).strip() == "":
                QMessageBox.information(self, "입력 오류", "결과 저장 필드명(result_field)을 지정해 주세요.", QMessageBox.Ok)
                return None

            breaks = _parse_breaks(setting_text)
            if not breaks:
                QMessageBox.information(
                    self, "입력 오류",
                    "구간 값(setting_text)이 올바르지 않습니다.\n"
                    "예: 0,100,200,300,400 처럼 숫자를 쉼표(,)로 구분해 2개 이상 입력해 주세요.",
                    QMessageBox.Ok
                )
                return None

            
            style = (setting_by_combo or "").strip().upper()
            use_alpha = ("A" in style)  
            use_numeric = ("1" in style) or ("NUM" in style)  
            if not (use_alpha or use_numeric):
                use_alpha = True  

            
            bins = breaks + [_np.inf]
            k = len(bins) - 1

            if use_alpha:
                labels = [_alpha_label(i) for i in range(1, k + 1)]
            else:
                labels = [str(i) for i in range(1, k + 1)]

            
            out = gdf.copy()

            
            
            
            try:
                series_raw = out[tuid]

                
                series_str = series_raw.astype(str).str.replace(",", "", regex=False).str.strip()

                dist_series = _pd.to_numeric(series_str, errors="coerce").fillna(0).astype(float)
            except Exception as e:
                QMessageBox.information(
                    self,
                    "작업 오류",
                    "해당 필드를 숫자 형식으로 변환할 수 없습니다.\n문자 또는 특수문자가 포함되어 있는지 확인해 주세요.",
                    QMessageBox.Ok
                )
                logger.error("에러 발생: %s", e, exc_info=True)
                return None

            
            cat = _pd.cut(
                dist_series,
                bins=bins,
                labels=labels,
                right=False,
                include_lowest=True
            )

            
            out[result_field] = cat.astype(object)
            out.loc[cat.isna(), result_field] = None

            return out.copy()

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

    
    
    

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

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

                    "show_uid_in_file": True,
                    "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": True,
                    "output_by_field": False,
                    "output_by_table": False,

                    "FILE_UID": [
                        '속성 필드 선택 (UID)',
                        'UID 필드 선택: ',
                        '최근접 이웃 객체의 속성값으로 저장할 UID 필드를 선택해 주세요.'
                    ],

                    "SETTING_NUMERIC": [
                        '최근접 객체 개수(N) 설정',
                        'N 값 입력: ',
                        '각 객체마다 계산할 최근접 이웃 객체의 개수(N)를 입력해 주세요.'
                    ],
                }
            if job_index == 4:
                option = {
                    "apply_basic_qss": True,

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

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

                    "setting_by_text": True,
                    "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": True, "items": ['A,B,C...', '1,2,3...']},

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

                    "FILE_TUID": [
                        '구간 분류 기준 거리 필드 선택',
                        '거리 필드 선택: ',
                        '구간 분류에 사용할 거리(숫자) 필드를 선택해 주세요. 예: dist'
                    ],

                    "SETTING_TEXT": [
                        '구간 경계값 설정',
                        '구간 값 입력: ',
                        '등급을 나눌 기준값을 쉼표(,)로 입력해 주세요.\n'
                        '예: 0,100,200,300,400\n'
                        '값의 의미: 0~99.999, 100~199.999, 200~299.999, 300~399.999, 400 이상'
                    ],

                    "SETTING_COMBO": [
                        '등급 스타일 선택',
                        '등급 스타일: ',
                        '구간에 부여할 등급 형식을 선택해 주세요. 예: A,B,C… 또는 1,2,3…'
                    ],

                    "RESULT_FIELD": [
                        '결과 필드 생성',
                        '결과 필드명 입력: ',
                        '구간 분류 결과를 저장할 새 필드명을 입력해 주세요. 예: dist_grade'
                    ],
                }

            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 == 3:
                result = self.calculate_same_layer_nearest_n_dist_and_uid(gdf, n=setting_numeric, uid=file_uid)

            elif self.job_index == 4:
                result = self.classify_distance_by_bins(gdf,
                                                        tuid=file_tuid,
                                                        setting_text=setting_text,
                                                        setting_by_combo=setting_combo,
                                                        result_field=result_field)

            
            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




