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

from collections.abc import Iterable
from datetime import datetime
from typing import TypeAlias

from common_hj3415.utils.time import utcnow
from contracts_hj3415.common.envelope import NfsEnvelopeDTO, make_nfs_envelope
from logging_hj3415 import logger
from scraper2_hj3415.app.nfs.domain.blocks import ListBlock
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.sinks.nfs_sink_port import NfsSinkPort
from scraper2_hj3415.app.nfs.services.fetch.fetch_c108 import FetchC108

endpoint_kind = EndpointKind.C108
endpoint = endpoint_kind.value

Row: TypeAlias = dict[str, str | int | None]


def _rows_to_report_list(block: ListBlock | None) -> list[Row]:
    """
    C108 RecordsBlock(rows) -> list[dict[str, str | int | None]]
    - C108은 레코드(행) 자체가 payload이므로, rows를 직렬화 안전한 dict로만 강제한다.
    - rows가 비정상이면 빈 리스트.
    """
    if block is None or not block.rows:
        return []

    out: list[Row] = []
    for r in block.rows:
        out.append(dict(r))
    return out


def c108_doc_to_envelope(*, doc: NfsDoc, asof: datetime) -> NfsEnvelopeDTO:
    """
    NfsDoc(domain) -> NfsEnvelopeDTO

    C108 규약(확정):
    - contents: {"리포트": list[C108ValuesMap]}
    - meta.labels: None (빈 dict가 아니라 None으로 통일)
    """
    if doc.endpoint_kind != EndpointKind.C108:
        raise ValueError(
            f"c108_doc_to_envelope expects C108 doc, got: {doc.endpoint_kind}"
        )
    contents: dict[str, list[Row]] = {"리포트": []}

    # block key는 '리포트' 하나가 핵심이지만, 정책상 get_block_keys(C108)를 따른다.
    report_rows: list[Row] = []
    for bk in get_block_keys(endpoint_kind):
        if bk != "리포트":
            continue
        block = doc.blocks.get(bk)
        report_rows = _rows_to_report_list(
            block if isinstance(block, ListBlock) else None
        )

    contents["리포트"] = report_rows

    return make_nfs_envelope(
        code=doc.code,
        asof=asof,
        endpoint=endpoint,
        contents=contents,
        labels=None,
    )


class IngestC108:
    fetch: FetchC108
    sink: NfsSinkPort

    def __init__(self, fetch: FetchC108, 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("doc:\n%s", doc)
        if doc is None:
            raise RuntimeError(f"c108 fetch returned None: code={code}")

        env = c108_doc_to_envelope(doc=doc, asof=asof)
        logger.debug("env:\n%s", 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 = [c108_doc_to_envelope(doc=d, asof=batch_asof) for d in docs]
        logger.debug("envs:\n%s", envs)

        await self.sink.write_many(envs)
        return envs
