# scraper2_hj3415/app/nfs/services/fetch/fetch_c108.py
from __future__ import annotations

import asyncio
import random
from collections.abc import Iterable, Mapping
from typing import Any

from logging_hj3415 import logger
from scraper2_hj3415.app.nfs.domain.blocks import ListBlock
from scraper2_hj3415.app.nfs.domain.constants import BLOCK_KEYS_BY_ENDPOINT
from scraper2_hj3415.app.nfs.domain.doc import NfsDoc
from scraper2_hj3415.app.nfs.domain.endpoint import EndpointKind
from scraper2_hj3415.app.nfs.domain.types import BlockKey, LabelsMap, Records
from scraper2_hj3415.app.nfs.parsing.c108_parser import parse_c108_to_dict
from scraper2_hj3415.app.nfs.ports.browser.browser_factory_port import (
    BrowserFactoryPort,
)
from scraper2_hj3415.app.nfs.services.fetch.common import gather_docs
from scraper2_hj3415.app.nfs.services.nfs_doc_builders import (
    build_records_block_from_rows,
)


def _as_records(x: Any) -> Records:  # pyright: ignore[reportExplicitAny, reportAny]
    """
    안전하게 rows를 Records(=Sequence[Record])로 캐스팅/정리.
    - None/비정상 값이면 빈 리스트
    - list[dict] 형태만 통과시키고 나머지는 필터
    """
    if not x:
        return []
    if not isinstance(x, list):
        return []

    out: list[dict[str, Any]] = []  # pyright: ignore[reportExplicitAny]
    for it in x:  # pyright: ignore[reportUnknownVariableType]
        if isinstance(it, dict):
            out.append(it)  # pyright: ignore[reportUnknownArgumentType]
    return out


def build_c108_doc_from_parsed(
    *,
    code: str,
    parsed: Mapping[str, Any],  # pyright: ignore[reportExplicitAny]
    block_keys: Iterable[BlockKey] | None = None,
    keep_empty_blocks: bool = True,
) -> NfsDoc:
    """
    c108 parser 결과(dict)를 받아서 NfsDoc(=RecordsBlock들)로 조립.

    규칙(너가 정한 원칙):
      - labels는 항상 존재(빈 dict라도)
      - c108은 labels를 비우는 것을 정상으로 간주

    keep_empty_blocks:
      - True: block은 항상 생성(rows 비어도 block 존재)
      - False: rows가 비면 blocks에서 제외
    """
    endpoint_kind = EndpointKind.C108

    if block_keys is None:
        # 보통 ("리포트",) 같은 튜플
        block_keys = BLOCK_KEYS_BY_ENDPOINT[endpoint_kind]

    blocks: dict[BlockKey, ListBlock] = {}
    labels: dict[BlockKey, LabelsMap] = {}

    for bk in block_keys:
        rows = _as_records(parsed.get(str(bk)))
        block = build_records_block_from_rows(
            endpoint_kind=endpoint_kind,
            block_key=bk,
            rows=rows,
        )

        if not keep_empty_blocks and not block.rows:
            continue

        blocks[bk] = block
        labels[bk] = {}  # c108은 labels 비우는 것이 정상

    return NfsDoc(
        code=code,
        endpoint_kind=endpoint_kind,
        blocks=blocks,
        labels=labels,
    )


class FetchC108:
    factory: BrowserFactoryPort

    def __init__(self, factory: BrowserFactoryPort):
        self.factory = factory

    async def _fetch_one(self, code: str, *, sleep_sec: float) -> NfsDoc | None:
        async with self.factory.lease() as browser:
            url = f"https://navercomp.wisereport.co.kr/v2/company/c1080001.aspx?cn=&cmp_cd={code}"
            await browser.goto_and_wait_for_stable(url, timeout_ms=10_000)

            if sleep_sec > 0:
                await asyncio.sleep(sleep_sec + random.uniform(0, 1.0))

            try:
                parsed = await parse_c108_to_dict(browser=browser)
            except Exception as e:
                url = await browser.current_url()
                if "Timeout" in type(e).__name__:
                    if await browser.count("#pageError") > 0:
                        logger.warning(f"c108 page not found | code={code} url={url}")
                    else:
                        logger.warning(f"c108 table timeout | code={code} url={url}")
                    return build_c108_doc_from_parsed(
                        code=code,
                        parsed={},  # 또는 {"리포트": []} (둘 중 하나로 통일)
                        keep_empty_blocks=True,
                    )
                raise

            logger.debug(f"parsed:\n{parsed}")
            block_keys = BLOCK_KEYS_BY_ENDPOINT[EndpointKind.C108]
            if not parsed or all(not (parsed.get(str(bk)) or []) for bk in block_keys):
                logger.info(f"c108 fetch: empty reports | code={code}")

                return build_c108_doc_from_parsed(
                    code=code,
                    parsed={},  # 또는 { "리포트": [] }
                    keep_empty_blocks=True,  # 이미 True 쓰고 있으니까 일관성 유지
                )

            doc = build_c108_doc_from_parsed(
                code=code, parsed=parsed, keep_empty_blocks=True
            )
            logger.debug(f"c108 doc: {doc}")
            return doc

    async def execute(self, code: str, *, sleep_sec: float = 2.0) -> NfsDoc | None:
        return await self._fetch_one(code, sleep_sec=sleep_sec)

    async def execute_many(
        self,
        codes: Iterable[str],
        *,
        sleep_sec: float = 2.0,
    ) -> list[NfsDoc]:
        code_list = list(codes)
        return await gather_docs(
            {c: self._fetch_one(c, sleep_sec=sleep_sec) for c in code_list},
            endpoint="c108",
        )
