# scraper2_hj3415/app/nfs/usecases/ingest/ingest_c101.py
from __future__ import annotations

from collections.abc import Iterable
from datetime import datetime
from typing import Any, cast

from common_hj3415.utils.time import utcnow
from contracts_hj3415.common.envelope import NfsEnvelopeDTO, make_nfs_envelope
from contracts_hj3415.nfs.c101 import C101Contents, ValuesMap
from contracts_hj3415.nfs.types import MetricKey
from logging_hj3415 import logger
from scraper2_hj3415.app.nfs.domain.blocks import KvBlock, ListBlock, MetricsBlock
from scraper2_hj3415.app.nfs.domain.constants import get_block_keys
from scraper2_hj3415.app.nfs.domain.doc import NfsDoc
from scraper2_hj3415.app.nfs.domain.endpoint import EndpointKind
from scraper2_hj3415.app.nfs.ports.ingest.nfs_ingest_port import NfsIngestPort
from scraper2_hj3415.app.nfs.ports.sinks.nfs_sink_port import NfsSinkPort
from scraper2_hj3415.app.nfs.services.fetch.fetch_c101 import FetchC101

endpoint_kind = EndpointKind.C101
endpoint = endpoint_kind.value


def _metrics_block_to_values_map(block: MetricsBlock) -> dict[MetricKey, ValuesMap]:
    """
    MetricsBlock(domain) -> dict[MetricKey, ValuesMap]
    ValuesMap: dict[PeriodKey, Num]
    """
    out: dict[MetricKey, ValuesMap] = {}
    for mk, series in block.metrics.items():
        # series.values: Mapping[Period, Num]  -> ValuesMap(PeriodKey->Num)
        out[mk] = dict(series.values)
    return out


def _unwrap_c101_block_to_contents_value(  # pyright: ignore[reportAny]
    block: Any,  # pyright: ignore[reportExplicitAny, reportAny]
) -> Any:  # pyright: ignore[reportExplicitAny]
    """
    C101의 domain block을 C101Contents에 들어갈 "순수 구조"로 변환.

    - KvBlock      -> dict[str, Scalar]  (Scalar: str|float|int|None)
    - RecordsBlock -> list[dict[str, Scalar]]
    - MetricsBlock -> dict[MetricKey, ValuesMap]  (펀더멘털/연간컨센서스 등에 사용 가능)
    - 기타         -> 그대로 반환(최후 방어)
    """
    if isinstance(block, KvBlock):
        # values: Mapping[str, Any] -> dict[str, Scalar]로 내려보내는 게 목표
        # (Scalar 제약은 타입 레벨이므로 런타임 강제는 하지 않음)
        return dict(block.values)

    if isinstance(block, ListBlock):
        # rows: Sequence[Record] -> list[dict[str, Scalar]]
        return [dict(r) for r in block.rows]

    if isinstance(block, MetricsBlock):
        return _metrics_block_to_values_map(block)

    return block  # pyright: ignore[reportAny]


def c101_doc_to_envelope(*, doc: NfsDoc, asof: datetime) -> NfsEnvelopeDTO:
    """
    NfsDoc(domain, endpoint=c101) -> NfsEnvelopeDTO(contracts)

    규약:
    - payload.meta.labels = None (C101은 labels 항상 None으로 통일)
    - payload.contents는 C101Contents 스키마를 따른다.
    - get_block_keys(C101) 기준으로 순회하되, 스키마 키만 채운다.
      (예: 파서/도메인이 추가 키를 내더라도 contents에는 고정 스키마만 실음)
    """
    # contents 기본 골격(스키마 고정)
    contents: dict[str, Any] = {  # pyright: ignore[reportExplicitAny]
        "요약": {},
        "시세": {},
        "주주현황": [],
        "기업개요": {},
        "펀더멘털": {},
        "어닝서프라이즈": {},
        "연간컨센서스": {},
    }

    # 도메인 블록키 기준으로 채우되, C101Contents 키에 해당하는 것만 반영
    for bk in get_block_keys(endpoint_kind):
        k = str(bk)
        if k not in contents:
            # C101Contents 스키마 밖이면 무시(경계 안정성)
            continue

        block = doc.blocks.get(bk)
        if block is None:
            # 스키마별 기본값 유지
            continue

        v = _unwrap_c101_block_to_contents_value(block)  # pyright: ignore[reportAny]

        # 스키마별 기본형을 최대한 유지(안전장치)
        if k == "주주현황":
            # list가 아니면 빈 리스트로
            contents[k] = v if isinstance(v, list) else []
        elif k in ("요약", "시세", "기업개요"):
            contents[k] = v if isinstance(v, dict) else {}
        elif k in ("펀더멘털", "연간컨센서스"):
            contents[k] = v if isinstance(v, dict) else {}
        elif k == "어닝서프라이즈":
            # dict[str, Any] 형태를 기대
            contents[k] = v if isinstance(v, dict) else {}
        else:
            # 혹시 확장 시
            contents[k] = v

    c101_contents: C101Contents = cast(C101Contents, cast(object, contents))

    # make_nfs_envelope가 payload.meta.endpoint/labels 세팅까지 해줌
    # labels는 C101 규약상 None으로 고정
    env = make_nfs_envelope(
        code=doc.code,
        asof=asof,
        endpoint=str(endpoint),
        labels=None,  # C101은 무조건 None
        contents=c101_contents,
    )
    return env


class IngestC101(NfsIngestPort):
    fetch: FetchC101
    sink: NfsSinkPort

    def __init__(self, fetch: FetchC101, sink: NfsSinkPort):
        self.fetch = fetch
        self.sink = sink

    async def execute(
        self,
        code: str,
        *,
        sleep_sec: float = 2.0,
        asof: datetime | None = None,
    ) -> NfsEnvelopeDTO:
        asof = asof or utcnow()

        doc = await self.fetch.execute(code, sleep_sec=sleep_sec)
        logger.debug(f"doc:\n{doc}")
        if doc is None:
            raise RuntimeError(f"c101 fetch returned None: code={code}")

        env = c101_doc_to_envelope(doc=doc, asof=asof)
        logger.debug(f"env:\n{env}")

        await self.sink.write(env)
        return env

    async def execute_many(
        self,
        codes: Iterable[str],
        *,
        sleep_sec: float = 2.0,
        asof: datetime | None = None,
    ) -> list[NfsEnvelopeDTO]:
        batch_asof = asof or utcnow()

        docs = await self.fetch.execute_many(codes, sleep_sec=sleep_sec)
        envs = [c101_doc_to_envelope(doc=d, asof=batch_asof) for d in docs]
        logger.debug(f"envs:\n{envs}")

        await self.sink.write_many(envs)
        return envs
