from DrissionPage import ChromiumPage, ChromiumOptions
from selectolax.parser import HTMLParser
import time
import re
from utils.logger import get_logger
from utils.path_utils import get_data_dir

class VOCScraper:
    def __init__(self, headless_mode=True, settings=None):
        """
        VOC 스크래퍼 초기화
        
        Args:
            headless_mode: 헤드리스 모드 여부
            settings: 설정 딕셔너리 (None일 경우 settings.json에서 로드)
        """
        self.logger = get_logger("Scraper")
        self.page = None
        self.headless_mode = headless_mode
        
        # 설정 로드 (전달받은 settings 또는 파일에서 로드)
        if settings is not None:
            self.settings = settings
        else:
            self.settings = self._load_settings()
        
        # 크롤링 설정 추출 (하드코딩 제거)
        crawl_settings = self.settings.get('crawling', {})
        self.target_depts = crawl_settings.get('target_depts', ['차량'])
        self.keywords = crawl_settings.get('keywords', [])
        self.filter_mode = crawl_settings.get('filter_mode', 'OR')  # 'AND' 또는 'OR'
        self.max_retries = crawl_settings.get('max_retries', 3)  # 재시도 횟수
        self.retry_delay = crawl_settings.get('retry_delay', 2)  # 재시도 간격(초)
    
    def _load_settings(self):
        """settings.json에서 설정 로드"""
        try:
            import json
            settings_file = get_data_dir() / "settings.json"
            if settings_file.exists():
                with open(settings_file, 'r', encoding='utf-8') as f:
                    return json.load(f)
            else:
                self.logger.warning("settings.json을 찾을 수 없습니다. 기본값 사용")
                return {}
        except Exception as e:
            self.logger.error(f"설정 파일 로드 실패: {e}")
            return {}

    def start_browser(self):
        """브라우저(Chromium) 실행 및 초기 설정"""
        if self.page: return True
        co = ChromiumOptions()
        
        # 1. 헤드리스 모드 설정 (User Setting)
        co.headless(self.headless_mode)
        co.auto_port()

        # 2. 브라우저 보안/자동화 감지 우회 설정
        # co.set_argument('--ignore-certificate-errors') # 인증서 에러 무시
        # co.set_argument('--no-sandbox')
        co.set_argument('--disable-gpu')
        
        # 3. 브라우저 불필요한 팝업 및 기능 비활성화
        co.set_pref('credentials_enable_service', False) # 비밀번호 저장 제안 끄기
        co.set_pref('profile.password_manager_enabled', False)
        
        # '자동제어 중' 알림바 제거
        co.set_argument('--disable-infobars') 
        
        # 콘솔 로그 레벨 조정 (에러만 출력)
        co.set_argument('--log-level=3')

        co.mute(True) # 오디오 음소거
        try:
            self.page = ChromiumPage(co)
            return True
        except Exception as e:
            self.logger.error(f"브라우저 실행 실패: {e}")
            return False

    def is_logged_in(self):
        """현재 로그인 상태인지 확인 (특정 요소 존재 여부로 판단)"""
        if not self.page: return False
        try:
            return bool(self.page.ele("css:td#m_td2", timeout=2))
        except:
            return False

    def login(self, uid, upw):
        """관리자 페이지 로그인 시도"""
        if not self.page: self.start_browser()
        try:
            self.logger.info(f"{uid} 계정으로 로그인 시도 중...")
            self.page.get("https://www.humetro.busan.kr/voc/admin/login_admin.jsp")
            
            # 입력 폼 대기 및 입력
            uid_field = self.page.ele("css:input[name='userID']", timeout=10)
            if uid_field:
                uid_field.input(uid)
                self.page.ele("css:input[name='password']").input(upw)
                self.page.ele("css:div.btn").click()
                
                # 로그인 성공 여부 확인
                if self.page.ele("css:td#m_td2", timeout=20):
                    self.logger.info("로그인 성공.")
                    time.sleep(1)
                    return True
            return False
        except Exception as e:
            self.logger.error(f"로그인 에러: {e}")
            return False

    def _check_data_exists(self):
        """현재 페이지에 데이터 행(리스트)이 실제 존재하는지 확인"""
        try:
            html = self.page.html
            tree = HTMLParser(html)
            rows = tree.css("tr")
            for row in rows:
                bg = row.attributes.get('bgcolor', '').lower()
                # 흰색 배경(#ffffff) 행이 실제 데이터 행임
                if bg == '#ffffff' and len(row.css('td')) > 5:
                    return True
            return False
        except:
            return False

    def _navigate_to_list_via_physical_click(self):
        """메뉴 호버 및 클릭을 통해 전체 VOC 목록으로 이동"""
        try:
            menu_td = self.page.ele("css:td#m_td2")
            if not menu_td: return False
            
            # 1. 메뉴 호버링으로 서브메뉴 띄우기
            menu_td.hover()
            time.sleep(0.5)
            
            # 2. '전체VOC보기' 링크 클릭
            target_link = self.page.ele("text:전체VOC보기")
            if target_link:
                target_link.click() 
                time.sleep(2) 
                
                # 혹시 데이터가 안 보이면 검색 버튼 명시적으로 클릭
                if not self._check_data_exists():
                    search_btn = self.page.ele("css:img[alt='검색']")
                    if search_btn:
                        search_btn.click()
                        time.sleep(2)
                return self._check_data_exists()
            return False
            return False
        except Exception as e:
            self.logger.warning(f"목록 페이지 이동 중 에러: {e}")
            return False

    def fetch_list_pages(self, max_pages_limit=0, keywords=None, target_depts=None):
        """VOC 목록 페이지를 순회하며 데이터 수집"""
        self.stop_requested = False
        all_results = []
        
        # 필터링 조건 설정
        keywords = keywords if keywords else []
        target_depts = target_depts if target_depts else self.target_depts

        try:
            # 0. 목록 페이지인지 확인하고 아니면 이동
            if "act=searchList" not in self.page.url or not self._check_data_exists():
                if not self._navigate_to_list_via_physical_click():
                    self.logger.error("목록 페이지 접근 실패. 수집을 중단합니다.")
                    return {"status": "error", "msg": "목록 페이지 접근 실패"}

            # 1. 페이지네이션 정보 확인
            total_cnt_ele = self.page.ele("text:page )", timeout=2)
            max_pages = 1
            if total_cnt_ele:
                # 예: ( 1 page / 7 page )
                txt = total_cnt_ele.text
                if "/" in txt:
                    try:
                        max_pages = int(txt.split("/")[1].replace("page", "").replace(")", "").strip())
                    except: pass
            
            # 수집 한도 설정 (인자 우선, 없으면 self.settings 등 참조해야 하나, 
            # 여기선 인자가 0이면 settings가 있을 경우 참조하는 식으로 구성)
            # 그러나 self.settings 직접 참조가 없으므로 인자에 의존하거나 동적 계산
            if max_pages_limit > 0:
                if max_pages > max_pages_limit:
                    max_pages = max_pages_limit
            
            self.logger.info(f"수집 대상 페이지 수: {max_pages}")

            # 2. 페이지 순회
            for page_num in range(1, max_pages + 1):
                if self.stop_requested: break
                
                self.logger.debug(f"페이지 {page_num} 수집 중...")
                if page_num > 1:
                    # 페이지 번호 클릭 ([2], [3]...)
                    # 자바스크립트 함수 호출이므로 text로 찾아서 클릭
                    page_btn = self.page.ele(f"text:[{page_num}]")
                    if page_btn:
                        page_btn.click()
                        time.sleep(1.5)
                    else: break

                # 현재 페이지 HTML 파싱
                tree = HTMLParser(self.page.html)
                
                # 배경색이 흰색인 행만 데이터 행으로 간주 (헤더 제외)
                rows = tree.css("tr[bgcolor='#ffffff']")

                for row in rows:
                    if self.stop_requested: break
                    cols = row.css("td")
                    if len(cols) < 7: continue

                    # [0]접수번호, [1]채널, [2]제목, [3]고객명, [4]부서, [5]공개, [6]상태
                    voc_id = cols[0].text(strip=True)
                    channel = cols[1].text(strip=True)
                    
                    # 제목: a 태그의 title 속성(전체 제목) 우선, 없으면 텍스트
                    title_a = cols[2].css_first("a")
                    title = title_a.attributes.get("title", "") if title_a else ""
                    if not title and title_a: title = title_a.text(strip=True)
                    
                    writer = cols[3].text(strip=True)
                    dept = cols[4].text(strip=True)
                    
                    is_public_txt = cols[5].text(strip=True)
                    is_public = 1 if is_public_txt == '○' or is_public_txt == 'O' else 0
                    
                    if is_public == 0:
                        self.logger.debug(f"비공개 게시글 생략: {voc_id}")
                        continue

                    status = cols[6].text(strip=True)

                    post_info = {
                        "id": voc_id,
                        "title": title,
                        "writer": writer,
                        "department": dept,
                        "is_public": is_public,
                        "status": status,
                        "channel": channel,
                        "is_related": 0
                    }

                    # 타겟 부서/키워드 필터링 (고도화된 로직)
                    is_target = self._check_filter_match(title, dept)
                    
                    post_info["is_related"] = 1 if is_target else 0

                    # 상세 수집 대상이면 (관심 게시글이거나 공개글이면)
                    # *여기서는 '상세 수집'을 별도로 하지 않고 목록 정보만 우선 저장*
                    # 상세 정보는 update_detail 로직이나 필요시점에 호출
                    # 단, 본문 내용 검색이 필요하다면 여기서 들어가야 함.
                    # 현재 요구사항은 알림이 중요하므로, 관련성이 있으면 상세를 긁는게 맞음.
                    
                    # 리스트 수집 시 상세 내용도 긁고 싶다면:
                    if is_target: # 관심 대상인 경우 상세까지 긁어서 정확도 높임
                        detail = self.fetch_detail_content(voc_id)
                        if detail:
                            # 상세에서 가져온 더 정확한 정보로 덮어쓰기
                            post_info.update(detail)
                            
                    all_results.append(post_info)

            return {"status": "success", "data": all_results}

        except Exception as e:
            self.logger.error(f"목록 수집 중 에러: {e}")
            return {"status": "error", "msg": str(e)}

    def fetch_detail_content(self, voc_id):
        """특정 VOC의 모든 상세 데이터를 추출 (제목, 역명, 질의내용, 답변, 첨부파일, 작성자 등)"""
        try:
            # 1. 목록에서 링크 찾기 시도
            target_link = self.page.ele(f"css:a[href*='vocCode={voc_id}']", timeout=1)
            
            navigated_directly = False

            if target_link:
                # 링크가 있으면 클릭
                target_link.click()
            else:
                # 링크가 없으면 (페이지가 넘어갔거나 등) 직접 URL 이동 시도
                # URL 패턴 추정: /voc/admin/view.jsp?vocCode={id}
                # 주의: 세션이 유지되어야 함
                direct_url = f"https://www.humetro.busan.kr/voc/admin/view.jsp?vocCode={voc_id}"
                self.logger.info(f"   -> 목록에서 링크 없음, 직접 이동 시도: {voc_id}")
                self.page.get(direct_url)
                navigated_directly = True

            # 2. 상세 페이지 로딩 대기
            # 성공 시 '질의내용' 텍스트 존재
            if self.page.ele("text:질의내용", timeout=10):
                tree = HTMLParser(self.page.html)
                data = {
                    "id": voc_id,
                    "title": "", "station": "", "content": "", "attachment": "",
                    "answer": "", "channel": "", "date": "", "voc_type": "",
                    "response_type": "", "summary": "",
                    "writer": "" # 작성자 추가
                }
                
                # 제목 추출
                title_a = tree.css_first("td.title, td[style*='font-weight:bold']") 
                if title_a: data["title"] = title_a.text(strip=True)

                # 라벨 기반 데이터 추출
                labels = tree.css("td[bgcolor='#E0EDEF']")
                for label in labels:
                    key = label.text(strip=True)
                    
                    parent = label.parent
                    if not parent: continue
                    tds = parent.css("td")
                    
                    idx = -1
                    for i, td in enumerate(tds):
                        if td == label:
                            idx = i
                            break
                    
                    if idx != -1 and idx + 1 < len(tds):
                        val_node = tds[idx+1]
                        val_txt = val_node.text(separator="\n", strip=True)

                        if "제목" in key and not data["title"]: data["title"] = val_txt
                        elif "역명" in key: data["station"] = val_txt
                        elif "질의내용" in key: data["content"] = val_txt
                        elif "첨부파일" in key: data["attachment"] = val_txt
                        elif "답변내용" in key: data["answer"] = val_txt
                        elif "접수채널" in key: data["channel"] = val_txt
                        elif "등록일자" in key: data["date"] = val_txt
                        elif "VOC유형" in key: data["voc_type"] = val_txt
                        elif "요약" in key: data["summary"] = val_txt
                        elif "고객명" in key: data["writer"] = val_txt # 작성자 추출
                        
                        elif "응답구분" in key:
                            checked_input = val_node.css_first("input[checked]")
                            if checked_input:
                                val = checked_input.attributes.get('value', '6')
                                mapping = {'1':'게시판', '2':'우편', '3':'팩스', '4':'전화', '5':'E-mail', '6':'기타'}
                                data["response_type"] = mapping.get(val, "기타")
                            else:
                                data["response_type"] = ""

                # 복귀 (직접 이동했으면 뒤로가기 혹은 목록으로 이동)
                if navigated_directly:
                    # 목록으로 명시적 이동보다는 뒤로가기가 안전할 수 있음
                    # 하지만 직접 이동했으므로 history.back()이 먹힐지 확인 필요.
                    # 일단 back() 시도
                    self.page.back()
                    
                    # 만약 back() 후에도 여전히 view 페이지면 강제 목록 이동
                    if "view.jsp" in self.page.url:
                         # 목록 URL로 이동 (검색 조건 유지 안될 수 있음 주의)
                         # self.page.get("https://www.humetro.busan.kr/voc/admin/list.jsp?act=searchList")
                         # 일단 back 한번 더 시도해보고 안되면 둠 (다음 루프에서 처리)
                         pass
                else:
                    self.page.back()
                    
                time.sleep(0.5)
                return data
            else:
                self.logger.error(f"   -> [Error] 상세 페이지 {voc_id} 로딩 실패/타임아웃. URL: {self.page.url}")
                if "vocCode=" in self.page.url:
                    self.page.back()
                return None

        except Exception as e:
            self.logger.error(f"   -> [Exception] {voc_id} Detail: {e}")
            if "vocCode=" in self.page.url:
                try: self.page.back()
                except: pass
            return None

    def download_attachment(self, voc_id, save_dir):
        """특정 VOC의 첨부파일을 다운로드합니다."""
        if not self.page: self.start_browser()
        
        try:
            # 1. 상세 페이지 이동 확인 (현재 아니면 이동)
            if f"vocCode={voc_id}" not in self.page.url:
                # 목록에서 찾아서 클릭 (이미 fetch_detail_content에서 쓰던 로직 재사용)
                target_link = self.page.ele(f"css:a[href*='vocCode={voc_id}']", timeout=5)
                if not target_link:
                    return None, "목록에서 게시글을 찾을 수 없습니다."
                target_link.click()
                self.page.wait.ele("text:질의내용")

            # 2. 첨부파일 링크 찾기
            # '첨부파일' 라벨 옆의 td 안의 a 태그 탐색
            label = self.page.ele("text:첨부파일", timeout=5)
            if not label:
                return None, "첨부파일 항목을 찾을 수 없습니다."
            
            # 레이아웃: td(라벨) -> td(값) -> a(링크)
            val_td = label.parent().next()
            link = val_td.ele("css:a")
            
            if not link:
                return None, "첨부파일 링크가 없습니다."

            # 3. 다운로드 시작
            filename = link.text
            self.logger.info(f"다운로드 중: {filename}...")
            
            # DrissionPage의 download 기능 사용
            # save_path가 디렉토리이면 그 안에 저장됨
            res = self.page.download(link, save_path=save_dir)
            
            if res:
                # res는 성공 시 (파일명, 경로) 튜플 혹은 성공여부 반환 (버전에 따라 다름)
                # 보통 (True, 최종완료경로) 형태
                final_path = res[1] if isinstance(res, tuple) else res
                return final_path, None
            else:
                return None, "다운로드에 실패했습니다."

        except Exception as e:
            return None, f"다운로드 중 예외 발생: {e}"
        finally:
            # 상세 페이지에서 다시 목록으로 (필요시)
            if "vocCode=" in self.page.url:
                self.page.back()
                time.sleep(0.5)

    def _check_filter_match(self, title: str, dept: str) -> bool:
        """
        고도화된 필터링 로직
        
        설정에 따라 AND/OR 모드로 동작하며, 단어 경계 매칭을 지원합니다.
        
        Args:
            title: 게시글 제목
            dept: 담당 부서
            
        Returns:
            bool: 필터링 조건 충족 여부
        """
        if not title:
            title = ""
        if not dept:
            dept = ""
        
        # 키워드 매칭 (단어 경계 고려)
        match_keyword = False
        if self.keywords:
            for kw in self.keywords:
                if not kw:
                    continue
                # 단순 substring 매칭 (기존 방식 유지하면서 개선)
                if kw in title:
                    match_keyword = True
                    self.logger.debug(f"키워드 매칭: '{kw}' in '{title[:30]}...'")
                    break
        
        # 부서 매칭 (정확한 매칭 또는 시작swith)
        match_dept = False
        if self.target_depts:
            for target in self.target_depts:
                if not target:
                    continue
                # 정확한 일치 또는 부서명이 타겟으로 시작/포함
                if target == dept or dept.startswith(target) or target in dept:
                    match_dept = True
                    self.logger.debug(f"부서 매칭: '{target}' in '{dept}'")
                    break
        
        # 필터 모드에 따른 결과 반환
        if self.filter_mode == "AND":
            # 키워드가 없으면 부서만, 부서가 없으면 키워드만 체크
            if self.keywords and self.target_depts:
                return match_keyword and match_dept
            elif self.keywords:
                return match_keyword
            elif self.target_depts:
                return match_dept
            else:
                return False
        else:  # OR 모드 (기본값)
            return match_keyword or match_dept

    def close(self):
        """브라우저 종료"""
        if self.page:
            self.page.quit()
