# scraper2_hj3415/app/nfs/adapters/site/wisereport_playwright.py
from __future__ import annotations

from logging_hj3415 import logger
from scraper2_hj3415.app.nfs.ports.browser.browser_port import BrowserPort
from scraper2_hj3415.app.nfs.ports.site.wisereport_port import WiseReportPort


def _parse_yyyy_mm(p: str) -> tuple[int, int] | None:
    try:
        y, m = p.split("/")
        return int(y), int(m)
    except Exception:
        return None


class WiseReportPlaywright(WiseReportPort):
    browser: BrowserPort

    def __init__(self, browser: BrowserPort):
        self.browser = browser

    async def _ensure_yearly_consensus_open_in_table_nth(
        self,
        *,
        table_selector: str,  # 예: TABLE_XPATH ("xpath=//div[@id='wrapper']//div//table")
        table_index: int,  # 예: TABLE_INDEX (2)
        after_click_sleep_ms: int = 150,
        max_rounds: int = 6,
        wait_timeout_sec: float = 12.0,
    ) -> bool:
        """
        목표: 연간 컨센서스 컬럼이 '반드시 펼쳐진 상태'가 되게 한다.
        전략:
          - TABLE_NTH 스코프 안에서
          - btn_moreY 또는 btn_moreQQ 이면서
          - '연간컨센서스보기' 텍스트를 가진 a 토글들 중
          - computedStyle(display) != 'none' 인 것들을 전부 클릭
          - 클릭마다 테이블 텍스트 변경을 기다림
        """

        table_scoped = f"{table_selector} >> nth={table_index}"

        # table 내부의 토글(a)만 잡기 (btn_moreY / btn_moreQQ 둘 다)
        VIEW_ALL = (
            f"{table_scoped} >> xpath=.//a["
            "("
            "contains(@class,'btn_moreY') or contains(@class,'btn_moreQQ')"
            ")"
            " and .//span[contains(normalize-space(.),'연간컨센서스보기')]"
            "]"
        )

        CLOSE_ALL = (
            f"{table_scoped} >> xpath=.//a["
            "("
            "contains(@class,'btn_moreY') or contains(@class,'btn_moreQQ')"
            ")"
            " and .//span[contains(normalize-space(.),'연간컨센서스닫기')]"
            "]"
        )

        # 테이블 텍스트 변화 감지용 “prev_text”
        prev_text = await self.browser.wait_table_text_changed(
            table_selector,
            index=table_index,
            prev_text=None,
            timeout_sec=wait_timeout_sec,
            min_lines=10,
        )

        logger.debug("ensure_yearly_consensus_open_in_table_nth: start")

        # round를 두는 이유:
        # - 보기 토글이 여러 개고, 클릭할 때 DOM이 재배치될 수 있음
        # - 1번에 다 못 누르면 다음 라운드에서 다시 스캔
        for round_no in range(1, max_rounds + 1):
            view_cnt = await self.browser.count(VIEW_ALL)
            close_cnt = await self.browser.count(CLOSE_ALL)
            logger.debug(
                f"round={round_no} toggle exists: view={view_cnt}, close={close_cnt}"
            )

            # "보기" 토글이 아예 없으면 -> 이미 다 펼쳐져 있거나(닫기만 존재),
            # 혹은 페이지 구조가 달라서 못 찾는 것. 여기서는 '성공'으로 간주.
            if view_cnt == 0:
                logger.debug("no VIEW toggles found in-table -> treat as OPEN")
                return True

            clicked_any = False

            # i를 0..view_cnt-1로 돌면서 display != none 인 것만 클릭
            # (중간에 DOM 바뀌면 count/순서가 바뀔 수 있으니, 실패해도 계속)
            for i in range(view_cnt):
                try:
                    # 혹시 DOM이 바뀌어 index가 사라졌으면 skip
                    if not await self.browser.is_attached(VIEW_ALL, index=i):
                        continue

                    disp = await self.browser.computed_style(
                        VIEW_ALL, index=i, prop="display"
                    )
                    if disp.strip().lower() == "none":
                        continue

                    # 화면 밖이면 클릭 실패할 수 있으니 스크롤
                    await self.browser.scroll_into_view(VIEW_ALL, index=i)

                    # trial(실패해도 진행)
                    _ = await self.browser.try_click(
                        VIEW_ALL, index=i, timeout_ms=1500, force=False
                    )

                    # 실제 클릭
                    try:
                        await self.browser.click(
                            VIEW_ALL, index=i, timeout_ms=4000, force=False
                        )
                    except Exception:
                        await self.browser.click(
                            VIEW_ALL, index=i, timeout_ms=4000, force=True
                        )

                    await self.browser.sleep_ms(after_click_sleep_ms)

                    # 클릭 후 테이블 텍스트 변경 대기
                    prev_text = await self.browser.wait_table_text_changed(
                        table_selector,
                        index=table_index,
                        prev_text=prev_text,
                        timeout_sec=wait_timeout_sec,
                        min_lines=10,
                    )

                    clicked_any = True
                    logger.debug(f"clicked VIEW toggle: idx={i}, display={disp}")

                except Exception as e:
                    logger.debug(
                        f"click VIEW toggle failed: idx={i}, err={type(e).__name__}: {e}"
                    )
                    continue

            # 이번 라운드에서 클릭을 하나도 못 했으면:
            # - 모든 VIEW가 display:none 이었거나
            # - 클릭이 막혔거나
            # => VIEW(display!=none)가 남아있는지 다시 검사
            if not clicked_any:
                remain = await self.browser.count(VIEW_ALL)
                logger.debug(f"no clicks in round; remain VIEW count={remain}")
                # VIEW는 있는데 전부 display:none 이면 사실상 '열림' 상태로 볼 수 있음
                # (닫기만 보이는 케이스)
                # 여기서는 “성공” 처리
                return True

            # 다음 라운드에서 다시 스캔해서 VIEW(display!=none)가 남아있으면 또 클릭
            # (다 눌렀으면 결국 클릭할 게 없어짐)

        # 라운드 다 돌았는데도 여기까지 왔다면,
        # “보기 토글이 계속 display!=none으로 남는다” = 열리지 않는 구조/권한/오버레이 등
        logger.warning("ensure_yearly_consensus_open_in_table_nth: exceeded max_rounds")
        return False

    async def _click_steps(
        self,
        steps: list[tuple[str, str]],
        *,
        jitter_sec: float = 0.6,
    ) -> None:
        for _name, selector in steps:
            await self.browser.wait_attached(selector)
            await self.browser.scroll_into_view(selector)

            logger.info(f"click step: {_name}")

            ok = await self.browser.try_click(selector, timeout_ms=1500, force=False)
            try:
                if ok:
                    await self.browser.click(selector, timeout_ms=4000, force=False)
                else:
                    await self.browser.click(selector, timeout_ms=4000, force=True)
            except Exception:
                await self.browser.click(selector, timeout_ms=4000, force=True)

            wait = int((0.5 + (jitter_sec * 0.5)) * 1000)
            await self.browser.sleep_ms(wait)

    async def _is_quarter_view_by_header(
        self, *, table_selector: str, table_index: int
    ) -> tuple[bool, list[str]]:
        periods = await self.browser.table_header_periods_mm_nth(
            table_selector, index=table_index
        )

        ym = [x for x in (_parse_yyyy_mm(p) for p in periods) if x is not None]
        if not ym:
            return False, periods  # 불확정이면 y 취급(재시도는 상위에서)

        months = {m for _, m in ym}

        # ✅ 월이 섞이면 거의 확실히 분기 화면
        if len(months) >= 2:
            return True, periods

        # ✅ 월이 하나뿐이면(03만/12만) 연간일 가능성이 높음
        #    (특히 결산월이 03인 종목의 y가 여기에 걸림)
        return False, periods

    async def set_view_c103(
        self,
        *,
        key: str,
        steps: list[tuple[str, str]],
        table_selector: str,
        table_index: int,
        max_attempts: int = 5,
        stabilize_timeout_sec: float = 10.0,
        code: str | None = None,
    ) -> None:
        """
        c103 화면을 원하는 상태(key)에 맞게 '확정'한다.
        - key.endswith("q") => 분기 화면이어야 함
        - key.endswith("y") => 연간 화면이어야 함  (즉 분기 시그널이 없어야 함)

        클릭 -> 안정화(wait_table_text_changed) -> 헤더 판정 -> 실패 시 복구/재시도.
        """
        want_q = key.endswith("q")
        last_periods: list[str] = []
        last_is_q: bool | None = None

        for attempt in range(1, max_attempts + 1):
            logger.info(f"set_view_c103: key={key} attempt={attempt} want_q={want_q}")

            # 1) 클릭 (가벼운 지터 포함)
            await self._click_steps(steps, jitter_sec=0.6)

            # 2) 토글 펼치기 (네 함수 그대로 사용)
            try:
                _ = await self._ensure_yearly_consensus_open_in_table_nth(
                    table_selector=table_selector,
                    table_index=table_index,
                )
            except Exception as e:
                logger.debug(f"ensure open failed (ignored): {type(e).__name__}: {e}")

            # 3) 렌더 안정화: '변경'은 보장 못하지만, 로딩 흔들림을 줄여줌
            try:
                _ = await self.browser.wait_table_text_changed(
                    table_selector,
                    index=table_index,
                    prev_text=None,
                    min_rows=4,
                    min_lines=30,
                    timeout_sec=stabilize_timeout_sec,
                    context={
                        "where": "set_view_c103",
                        "key": key,
                        "want_q": want_q,
                        "table_index": table_index,
                        "attempt": attempt,
                        "code": code,
                    },
                )
            except Exception as e:
                logger.debug(
                    f"stabilize wait failed (ignored): {type(e).__name__}: {e}"
                )

            # 4) 헤더로 상태 확정
            is_q, periods = await self._is_quarter_view_by_header(
                table_selector=table_selector,
                table_index=table_index,
            )
            last_is_q, last_periods = is_q, periods

            logger.info(
                f"set_view_c103: key={key} header periods(head)={periods[:8]} is_q={is_q}"
            )

            if want_q == is_q:
                return

            # 5) 실패 시 복구 전략
            # attempt가 올라갈수록 강하게: 스크롤/force click은 click_steps 쪽을 강화하는 게 좋지만
            # 여기서는 reload로 리셋을 걸어준다.
            if attempt in (2, 4):
                logger.warning(
                    f"set_view_c103 mismatch -> reload | key={key} attempt={attempt}/{max_attempts}"
                )
                await self.browser.reload(timeout_ms=12_000)
            await self.browser.sleep_ms(250)

        raise RuntimeError(
            f"set_view_c103 failed: key={key} want_q={want_q} last_is_q={last_is_q} "  # pyright: ignore[reportImplicitStringConcatenation]
            f"last_periods={last_periods[:12]}"
        )

    async def set_view_c104(
        self,
        *,
        key: str,
        steps: list[tuple[str, str]],
        table_selector: str,
        table_index: int,
        prev_text_by_idx: dict[int, str | None],
        max_attempts: int = 5,
        stabilize_timeout_sec: float = 10.0,
        min_rows: int = 4,
        min_lines: int = 30,
        open_consensus: bool = True,
        code: str | None = None,
    ) -> None:
        """
        c104 화면을 원하는 상태(key)에 맞게 '확정'한다.

        - key.endswith("q") => 분기 화면이어야 함 (헤더에 03/06/09 존재)
        - key.endswith("y") => 연간 화면이어야 함 (헤더에 03/06/09 없어야 함, 12는 보통 존재)

        절차:
        1) 클릭 steps
        2) (옵션) 연간컨센서스 펼치기
        3) wait_table_text_changed (idx별 prev_text 추적)
        4) header periods로 q/y 판정
        5) mismatch면 reload/재시도
        """
        want_q = key.endswith("q")

        # idx별 prev_text 초기값 방어
        if table_index not in prev_text_by_idx:
            prev_text_by_idx[table_index] = None

        last_periods: list[str] = []
        last_is_q: bool | None = None

        for attempt in range(1, max_attempts + 1):
            logger.info(
                f"set_view_c104: key={key} idx={table_index} attempt={attempt} want_q={want_q}"
            )

            # 1) 클릭(행동)
            await self._click_steps(steps, jitter_sec=0.6)

            # 2) 컨센서스 펼치기(옵션)
            if open_consensus:
                try:
                    _ = await self._ensure_yearly_consensus_open_in_table_nth(
                        table_selector=table_selector,
                        table_index=table_index,
                        wait_timeout_sec=stabilize_timeout_sec,
                    )
                except Exception as e:
                    logger.debug(
                        f"ensure open failed (ignored): {type(e).__name__}: {e}"
                    )

            # 3) 안정화(변경/유효 텍스트 대기) - idx별로 prev 추적
            try:
                prev_text_by_idx[table_index] = (
                    await self.browser.wait_table_text_changed(
                        table_selector,
                        index=table_index,
                        prev_text=prev_text_by_idx[table_index],
                        min_rows=min_rows,
                        min_lines=min_lines,
                        timeout_sec=stabilize_timeout_sec,
                        context={
                            "where": "set_view_c104",
                            "key": key,
                            "want_q": want_q,
                            "table_index": table_index,
                            "attempt": attempt,
                            "code": code,
                        },
                    )
                )
            except Exception as e:
                logger.debug(
                    f"stabilize wait failed (ignored): {type(e).__name__}: {e}"
                )

            # 4) 헤더로 상태 확정
            is_q, periods = await self._is_quarter_view_by_header(
                table_selector=table_selector,
                table_index=table_index,
            )
            last_is_q, last_periods = is_q, periods

            logger.info(
                f"set_view_c104: key={key} idx={table_index} periods(head)={periods[:8]} is_q={is_q}"
            )

            # periods 자체가 비면 "불확정"으로 보고 재시도하는 게 안전
            if not periods:
                logger.warning(
                    f"set_view_c104: header periods empty -> retry | key={key} idx={table_index}"
                )
            else:
                if want_q == is_q:
                    return  # ✅ 성공 확정

            # 5) 실패 복구 전략
            if attempt in (2, 4):
                logger.warning(
                    f"set_view_c104 mismatch -> reload | key={key} attempt={attempt}/{max_attempts}"
                )
                await self.browser.reload(timeout_ms=12_000)

            await self.browser.sleep_ms(250)

        raise RuntimeError(
            f"set_view_c104 failed: key={key} idx={table_index} want_q={want_q} "  # pyright: ignore[reportImplicitStringConcatenation]
            f"last_is_q={last_is_q} last_periods={last_periods[:8]}"
        )
