
import os
import re
from pathlib import Path
from typing import Optional, Dict, List, Tuple


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


from qgis.core import QgsVectorLayer


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


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


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

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

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


from urbanq.menu.autoUI.fileRread_dockwidget import fileRreadDockWidget
from urbanq.menu.autoUI.fileSave_dockwidget import fileSaveDockWidget
from urbanq.menu.autoUI.fileSetting_dockwidget import fileSettingDockWidget
from urbanq.menu.autoUI.ImageDescription_dockwidget import ImageDescriptionDockWidget
from urbanq.menu.spatialAnalysis.AddressConversion.address_conversion_client import batch_geocode


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


class AddressEditingDockWidget(QDialog, FORM_CLASS):  
    def __init__(self, parent=None):
        
        super(AddressEditingDockWidget, 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 normalize_address_field(
            self,
            gdf,
            source_field: str,
            output_field: str,
    ):
        

        
        PUNCT_RE = re.compile(r"[,\(\)\[\]\{\}]")
        SPACE_RE = re.compile(r"\s+")

        
        LOT_RE = re.compile(r"(?:^|\s)(산\s*)?\d+(?:-\d+)?(?=\s|$)")

        
        BLOCK_RE = re.compile(r"^\s*(동|층|호|단지|블록|차|관|라인|타워|빌딩)")

        def normalize_jibun(text: Optional[str]) -> Optional[str]:
            if not text:
                return None

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

            
            s = PUNCT_RE.sub(" ", s)
            s = SPACE_RE.sub(" ", s).strip()

            
            matches = list(LOT_RE.finditer(s))
            if not matches:
                return s

            chosen = None
            for m in reversed(matches):
                tail = s[m.end():]

                
                if BLOCK_RE.match(tail):
                    continue

                chosen = m
                break

            if not chosen:
                chosen = matches[-1]

            return s[:chosen.end()].strip()

        def normalize_text_cell(v):
            if v is None or v is pd.NA:
                return pd.NA
            if isinstance(v, float) and pd.isna(v):
                return pd.NA
            if isinstance(v, str):
                s = v.strip()
                if s == "":
                    return pd.NA
                return normalize_jibun(s)
            return v

        try:
            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf.copy()

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

            
            gdf_attr = normalize_null_values(gdf_attr)

            
            if output_field not in gdf_copy.columns:
                gdf_copy[output_field] = pd.NA

            
            gdf_attr[output_field] = gdf_attr[source_field].map(normalize_text_cell)

            
            gdf_copy[output_field] = gdf_attr[output_field]

            return gdf_copy

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

    
    def convert_jibun_to_pnu(self, gdf, source_field: str, output_field: str):
        
        try:
            
            geometry_col = gdf.geometry.name

            
            gdf_copy = gdf.copy()

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

            
            gdf_copy_not_null_values = normalize_null_values(gdf_copy_no_geom)

            
            base_dir = os.path.dirname(os.path.abspath(__file__))
            code_csv = os.path.join(base_dir, "data", "국토교통부_법정동코드_20250805.csv")

            try:
                dong_df = pd.read_csv(code_csv, dtype=str, encoding="euc-kr")
            except UnicodeDecodeError:
                dong_df = pd.read_csv(code_csv, dtype=str, encoding="utf-8")

            
            needed = {"법정동코드", "법정동명"}
            if not needed.issubset(dong_df.columns):
                QMessageBox.information(
                    self,
                    "컬럼 오류",
                    f"법정동코드 CSV에 필요한 컬럼이 없습니다.\n필요: {sorted(needed)}\n현재: {list(dong_df.columns)}",
                    QMessageBox.Ok
                )
                return None

            
            
            
            SPACE_RE = re.compile(r"\s+")
            PUNCT_RE = re.compile(r"[,\(\)\[\]\{\}·ㆍ•]")  
            
            NORMALIZE_MAP = {
                "강원도": "강원특별자치도",
                "전라북도": "전북특별자치도",  
            }

            def _clean_text(s: str) -> str:
                if s is None:
                    return ""
                x = str(s).strip()
                if not x:
                    return ""
                x = PUNCT_RE.sub(" ", x)
                x = SPACE_RE.sub(" ", x).strip()
                
                for k, v in NORMALIZE_MAP.items():
                    if x.startswith(k + " "):
                        x = v + x[len(k):]
                return x

            
            dong_df["법정동명"] = dong_df["법정동명"].astype(str).map(_clean_text)
            dong_df["법정동코드"] = dong_df["법정동코드"].astype(str).str.strip()

            
            dong_map: Dict[str, str] = dict(zip(dong_df["법정동명"], dong_df["법정동코드"]))

            
            
            tail_index: Dict[str, List[Tuple[str, str]]] = {}
            for name, code in zip(dong_df["법정동명"], dong_df["법정동코드"]):
                parts = name.split(" ")
                tail = parts[-1] if parts else ""
                if not tail:
                    continue
                tail_index.setdefault(tail, []).append((name, code))

            
            for tail in tail_index:
                tail_index[tail].sort(key=lambda x: len(x[0]), reverse=True)

            
            
            
            LOT_RE = re.compile(r"(산\s*)?\d+(?:-\d+)?")  
            BLOCK_RE = re.compile(r"^\s*(동|층|호|단지|블록|차|관|라인|타워|빌딩)\b")

            def _extract_jibun(addr: str) -> str:
                
                s = _clean_text(addr)
                if not s:
                    return ""

                
                parts = s.split(" ")
                if "산" in parts:
                    i = len(parts) - 1 - parts[::-1].index("산")  
                    if i < len(parts) - 1:
                        
                        m = LOT_RE.search(parts[i + 1])
                        if m:
                            return f"산 {m.group(0).replace(' ', '')}".replace("산", "산 ")
                        return f"산 {parts[i+1]}"
                    return "산 0"

                
                matches = list(LOT_RE.finditer(s))
                if not matches:
                    
                    return s.split(" ")[-1]

                chosen = None
                for m in reversed(matches):
                    
                    tail = s[m.end():]
                    if not BLOCK_RE.match(tail):
                        chosen = m
                        break
                if chosen is None:
                    chosen = matches[-1]

                token = s[chosen.start():chosen.end()].strip()
                
                token = re.sub(r"^산\s*", "산 ", token)
                return token

            def _parcel_type(jibun_token: str) -> str:
                return "2" if str(jibun_token).strip().startswith("산") else "1"

            def _extract_legal_dong(full_addr: str, jibun_token: str) -> str:
                s = _clean_text(full_addr)
                jt = _clean_text(jibun_token)
                if not s:
                    return ""
                if not jt:
                    return s

                idx = s.find(jt)
                if idx == -1:
                    
                    parts = s.split(" ")
                    return " ".join(parts[:-1]).strip() if len(parts) >= 2 else s
                return s[:max(0, idx - 1)].strip()

            def _extract_main_number(jibun_token: str) -> str:
                t = str(jibun_token).replace("산", "").strip()
                if "-" in t:
                    main = t.split("-")[0]
                else:
                    main = t
                nums = re.findall(r"\d+", main)
                if not nums:
                    return "0000"
                return ("0000" + nums[0])[-4:]

            def _extract_sub_number(jibun_token: str) -> str:
                t = str(jibun_token).strip()
                if "-" in t:
                    sub = t.split("-")[1]
                else:
                    sub = ""
                nums = re.findall(r"\d+", sub)
                if not nums:
                    return "0000"
                return ("0000" + nums[0])[-4:]

            
            
            
            def _get_legal_dong_code(legal_dong_name_raw: str) -> str:
                
                legal_dong_name = _clean_text(legal_dong_name_raw)
                if not legal_dong_name:
                    return "0000000000"

                
                code = dong_map.get(legal_dong_name)
                if code:
                    return code

                
                parts = legal_dong_name.split(" ")
                tail = parts[-1] if parts else ""
                candidates = tail_index.get(tail, [])

                if not candidates:
                    return "0000000000"

                
                
                for cand_name, cand_code in candidates:
                    if cand_name and cand_name in legal_dong_name:
                        return cand_code

                
                
                best_code = "0000000000"
                best_score = -1
                for cand_name, cand_code in candidates:
                    score = 0
                    
                    cand_tokens = set(cand_name.split(" "))
                    in_tokens = set(legal_dong_name.split(" "))
                    score += len(cand_tokens & in_tokens)
                    if score > best_score:
                        best_score = score
                        best_code = cand_code

                return best_code if best_score >= 2 else "0000000000"

            
            
            
            def _make_pnu(addr: Optional[str]) -> str:
                
                if addr is None:
                    return "0" * 19
                s = str(addr).strip()
                if not s:
                    return "0" * 19

                jb = _extract_jibun(s)
                if not jb:
                    return "0" * 19

                pj = _parcel_type(jb)
                bzd = _extract_legal_dong(s, jb)
                bb = _extract_main_number(jb)
                pb = _extract_sub_number(jb)
                cd = _get_legal_dong_code(bzd)

                if cd == "0000000000":
                    return "0" * 19

                return f"{cd}{pj}{bb}{pb}"

            
            
            
            def get_unique_field(base_name: str) -> str:
                name = base_name
                count = 0
                while name in gdf_copy.columns:
                    count += 1
                    name = base_name + ("_" * count)
                return name

            out_col = output_field if output_field else "PNU"
            out_col = get_unique_field(out_col) if out_col in gdf_copy.columns else out_col

            
            gdf_copy[out_col] = gdf_copy_not_null_values[source_field].apply(_make_pnu).astype(str)

            return gdf_copy

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

    
    
    

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

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

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

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

                    "FILE_TUID": [
                        '주소 정제 기준 필드 선택',
                        '필드 선택: ',
                        '선택한 주소 필드에서 건물명, 괄호, 동·호수 등 불필요한 정보를 제거하여 표준화된 주소로 정제합니다.'
                    ],

                    "RESULT_FIELD": [
                        '주소 정제 결과 필드 생성',
                        '필드명 입력: ',
                        ''
                    ],
                }

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

                    "FILE_TUID": [
                        '지번주소 기준 필드 선택',
                        '필드 선택: ',
                        '선택한 지번주소를 부동산 행정 관리용 고유번호(PNU)로 변환합니다.'
                    ],

                    "RESULT_FIELD": [
                        'PNU 결과 필드 생성',
                        '필드명 입력: ',
                        ''
                    ],
                }

            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

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

            elif self.job_index == 1:
                gdf = keep_columns_gdf(gdf, file_field_selection) if file_is_field_check else gdf
                result = self.convert_jibun_to_pnu(gdf, file_tuid, 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




