# scraper2_hj3415/app/nfs/services/nfs_doc_builders.py
# pyright: reportExplicitAny=false
from __future__ import annotations

from collections import defaultdict
from collections.abc import Iterable, Mapping
from typing import Any

from common_hj3415.utils import nan_to_none
from scraper2_hj3415.app.nfs.domain.blocks import (
    BlockData,
    KvBlock,
    ListBlock,
    MetricsBlock,
    MetricSeries,
)
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,
    MetricKey,
    Num,
    Period,
    Records,
)


def is_all_none(row: dict[str, Any]) -> bool:
    return all(v is None for v in row.values())  # pyright: ignore[reportAny]


def build_metrics_block_and_labels_from_rows(
    *,
    endpoint_kind: EndpointKind,
    block_key: BlockKey,
    rows: Records,
    item_key: str = "항목",
    raw_label_key: str = "항목_raw",
) -> tuple[MetricsBlock, LabelsMap]:
    """
    rows(list[dict]) -> (MetricsBlock, LabelsMap)
    c103/c104/c106 공통 빌더.

    - Metric key는 item_key(보통 '항목')에서 만들고,
      기간 컬럼들은 {Period: Num}으로 유지한다.
    - LabelsMap은 dto_key -> raw_label(정제된 원라벨)
    """
    grouped: dict[str, list[tuple[dict[Period, Num], str]]] = defaultdict(list)

    for r in rows:
        item = r.get(item_key)
        if not item:
            continue

        raw_label = r.get(raw_label_key)
        if raw_label is None:
            raw_label = item  # pyright: ignore[reportAny]

        per_map: dict[Period, Num] = {}

        for key, value in r.items():  # pyright: ignore[reportAny]
            # 메타 컬럼은 건너뛴다
            if key in (item_key, raw_label_key):
                continue

            period: Period = str(key)
            num: Num = nan_to_none(value)  # pyright: ignore[reportAny]

            per_map[period] = num

        grouped[item].append((per_map, raw_label))

    series_map: dict[MetricKey, MetricSeries] = {}
    labels_map: LabelsMap = {}

    for item, pairs in grouped.items():
        if len(pairs) == 1:
            per_map, raw_label = pairs[0]
            series_map[item] = MetricSeries(key=item, values=per_map)
            labels_map[item] = raw_label
            continue

        kept = [(per_map, raw) for (per_map, raw) in pairs if not is_all_none(per_map)]
        if not kept:
            continue

        for idx, (per_map, raw_label) in enumerate(kept, start=1):
            mk = item if idx == 1 else f"{item}_{idx}"
            series_map[mk] = MetricSeries(key=mk, values=per_map)
            labels_map[mk] = raw_label

    block = MetricsBlock(
        endpoint_kind=endpoint_kind, block_key=block_key, metrics=series_map
    )
    return block, labels_map


def build_metrics_doc_from_parsed(
    *,
    code: str,
    endpoint_kind: EndpointKind,
    parsed: Mapping[str, Any],
    block_keys: Iterable[BlockKey] | None = None,
    item_key: str = "항목",
    raw_label_key: str = "항목_raw",
    keep_empty_blocks: bool = True,
) -> NfsDoc:
    """
    parser가 만든 dict(블록키 -> rows)를 받아서 NfsDoc(=MetricsBlock들)로 조립.
    - c103/c104/c106 공용으로 사용 가능.

    keep_empty_blocks:
      - True: block은 항상 생성 (metrics 비어도 block 존재)
      - False: rows가 없거나 metrics가 비면 blocks에서 제외
    """
    if block_keys is None:
        block_keys = BLOCK_KEYS_BY_ENDPOINT[endpoint_kind]

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

    for bk in block_keys:
        rows: list[Any] = parsed.get(str(bk), []) or []
        block, lm = build_metrics_block_and_labels_from_rows(
            endpoint_kind=endpoint_kind,
            block_key=bk,
            rows=rows,
            item_key=item_key,
            raw_label_key=raw_label_key,
        )

        if not keep_empty_blocks and not block.metrics:
            continue

        blocks[bk] = block
        labels[bk] = lm  # 비어있어도 {}로 유지

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


def build_records_block_from_rows(
    *,
    endpoint_kind: EndpointKind,
    block_key: BlockKey,
    rows: Records,
) -> ListBlock:
    """
    rows(list[dict]) -> RecordsBlock
    - c108 같은 레코드성 블록(리포트 목록 등)에 사용
    """
    # RecordsBlock 쪽에서도 __post_init__로 block_key 검증이 수행된다는 전제(네가 정돈한 도메인)
    return ListBlock(endpoint_kind=endpoint_kind, block_key=block_key, rows=list(rows))


def build_kv_block_from_mapping(
    *,
    endpoint_kind: EndpointKind,
    block_key: BlockKey,
    data: Mapping[str, Any] | None,
    keep_empty: bool = True,
) -> KvBlock | None:
    """
    dict(매핑) 형태의 블록 데이터를 KvBlock으로 감싼다.

    - data가 비어있거나 None이면:
      - keep_empty=False: None 반환
      - keep_empty=True : 빈 dict로 KvBlock 생성
    - c101의 요약/시세/기업개요/펀더멘털/어닝서프라이즈 같은 dict 구조에 사용
    """
    if not data:
        if not keep_empty:
            return None
        data = {}

    return KvBlock(endpoint_kind=endpoint_kind, block_key=block_key, values=data)


def build_c101_doc_from_parsed(
    *,
    code: str,
    parsed: Mapping[str, Any],
    block_keys: Iterable[BlockKey] | None = None,
    keep_empty_blocks: bool = True,
) -> NfsDoc:
    """
    c101 parser 결과(블록별 다양한 타입)를 NfsDoc으로 조립.
    labels는 c101은 '비어도 정상' 규칙을 따르므로 항상 {}로 둔다.
    """
    endpoint_kind = EndpointKind.C101

    if block_keys is None:
        block_keys = BLOCK_KEYS_BY_ENDPOINT[endpoint_kind]

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

    for bk in block_keys:
        v: list[Any] | dict[str, Any] | None = parsed.get(str(bk))

        # c101 규칙: labels는 비어도 정상, 있으면 넣는 게 아니라 "기본 비움" 추천
        labels[bk] = {}

        # list -> ListBlock
        if isinstance(v, list):
            # v: list[dict[str, Any]] 가정 (파서가 그렇게 만들고 있음)
            rb = build_records_block_from_rows(
                endpoint_kind=endpoint_kind,
                block_key=bk,
                rows=v,
            )
            blocks[bk] = rb
            continue

        # dict(중첩 포함) -> KvBlock
        if isinstance(v, dict):
            kb = build_kv_block_from_mapping(
                endpoint_kind=endpoint_kind,
                block_key=bk,
                data=v,
                keep_empty=keep_empty_blocks,
            )
            if kb is not None:
                blocks[bk] = kb
            continue

        # None/기타 -> empty policy
        if keep_empty_blocks:
            kb = build_kv_block_from_mapping(
                endpoint_kind=endpoint_kind,
                block_key=bk,
                data={},
                keep_empty=True,
            )
            if kb is not None:
                blocks[bk] = kb

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