from __future__ import annotations

import time
from collections.abc import Iterable, Sequence
from datetime import datetime, timezone
from typing import Annotated, cast, get_args

import typer
from contracts_hj3415.common.types import Endpoint
from db2_hj3415.universe.repo import list_latest_universe_codes
from logging_hj3415 import (
    current_log_level,
    reset_logging,
    setup_logging,
    to_pretty_json,
)
from scraper2_hj3415.app.composition import AppUsecases, build_usecases
from scraper2_hj3415.app.nfs.ports.ingest.nfs_ingest_port import NfsIngestPort
from scraper2_hj3415.app.settings import Settings, get_settings

# -----------------------------------------------------------------------------
# logging bootstrap
# -----------------------------------------------------------------------------

_LOGGING_READY = False


def configure_logging() -> None:
    global _LOGGING_READY
    if _LOGGING_READY:
        return
    setup_logging()
    reset_logging(get_settings().log_level)
    print(f"Current log level - {current_log_level()}")
    _LOGGING_READY = True


# -----------------------------------------------------------------------------
# parameter aliases
# -----------------------------------------------------------------------------

ArgCode = Annotated[str, typer.Argument(help="종목코드 (예: 005930)")]
ArgEndpoint = Annotated[str, typer.Argument(help="c101|c103|c104|c106|c108|all")]

OptShow = Annotated[bool, typer.Option(help="결과 출력")]
OptAsOf = Annotated[
    str | None,
    typer.Option(help="배치 기준시각(UTC 권장). 예: 2026-01-09T05:00:00Z"),
]
OptUniverse = Annotated[str, typer.Option(help="유니버스 이름")]
OptLimit = Annotated[int, typer.Option(help="0=전체")]
OptChunk = Annotated[int, typer.Option(help="진행률 표시용 배치 크기")]

OptDartSDate = Annotated[
    str,
    typer.Option("--sdate", help="조회 시작일 (YYYYMMDD)"),
]
OptDartEDate = Annotated[
    str,
    typer.Option("--edate", help="조회 종료일 (YYYYMMDD)"),
]

# -----------------------------------------------------------------------------
# small helpers
# -----------------------------------------------------------------------------


def get_cli_settings() -> Settings:
    return get_settings()


def ensure_non_empty_code(code: str) -> str:
    c = code.strip()
    if not c:
        raise typer.BadParameter("code는 비어있을 수 없습니다.")
    return c


def iter_endpoints(endpoint: str) -> list[str]:
    """
    endpoint arg를 ingest 속성명들("c101"...)로 변환.
    """
    ep = endpoint.strip().lower()
    if ep == "all":
        return list(get_args(Endpoint))

    valid = set(get_args(Endpoint))
    if ep not in valid:
        raise typer.BadParameter(
            f"endpoint must be one of {sorted(valid)} or 'all'. got={endpoint!r}"
        )
    return [ep]


def parse_asof(asof: str | None) -> datetime:
    """
    ISO8601 string -> UTC aware datetime.
    """
    if not asof or not asof.strip():
        return datetime.now(timezone.utc)

    s = asof.strip()
    if s.endswith("Z"):
        s = s[:-1] + "+00:00"

    try:
        dt = datetime.fromisoformat(s)
    except ValueError as e:
        raise typer.BadParameter(
            f"--asof must be ISO8601 datetime (e.g. 2026-01-09T05:00:00Z). got={asof!r}"
        ) from e

    if dt.tzinfo is None:
        dt = dt.replace(tzinfo=timezone.utc)

    return dt.astimezone(timezone.utc)


def format_elapsed(sec: float) -> str:
    if sec < 60:
        return f"{sec:.1f}s"
    m, s = divmod(int(sec), 60)
    if m < 60:
        return f"{m}m {s}s"
    h, m = divmod(m, 60)
    return f"{h}h {m}m {s}s"


def chunks(xs: Sequence[str], n: int) -> Iterable[list[str]]:
    if n <= 0:
        yield list(xs)
        return
    for i in range(0, len(xs), n):
        yield list(xs[i : i + n])


def get_ingest_uc(ucs: AppUsecases, ep: str) -> NfsIngestPort:
    try:
        return cast(NfsIngestPort, getattr(ucs.nfs.ingest, ep))
    except AttributeError as e:
        raise RuntimeError(f"ingest usecase not found: {ep!r}") from e


def build_cli_usecases(*, nfs_sink: str | None = None) -> AppUsecases:
    configure_logging()
    if nfs_sink is None:
        return build_usecases()
    return build_usecases(nfs_sink=nfs_sink)


async def load_universe_codes(
    *,
    ucs: AppUsecases,
    universe: str,
    limit: int = 0,
) -> list[str]:
    db = ucs.resources.db
    codes = sorted(await list_latest_universe_codes(db, universe=universe))
    if limit > 0:
        codes = codes[:limit]
    return codes


async def run_ingest_many_with_progress(
    *,
    ucs: AppUsecases,
    endpoints: list[str],
    codes: list[str],
    sleep_sec: float,
    asof: datetime,
    chunk_size: int,
    progress_every: int,
) -> None:
    total = len(codes)
    if total == 0:
        typer.echo("(no codes)")
        return

    t0 = time.perf_counter()

    for ep in endpoints:
        ingest_uc = get_ingest_uc(ucs, ep)

        ok = 0
        fail = 0
        typer.echo(f"\n=== START: {ep} === total={total}, chunk_size={chunk_size}")

        for idx, batch in enumerate(chunks(codes, chunk_size), start=1):
            try:
                results = await ingest_uc.execute_many(
                    batch,
                    sleep_sec=sleep_sec,
                    asof=asof,
                )
                ok += len(results)
            except Exception as e:
                fail += len(batch)
                typer.echo(f"[WARN] batch failed: {type(e).__name__}: {e}")

            done = min(idx * chunk_size, total)
            if progress_every > 0 and (idx % progress_every == 0 or done == total):
                typer.echo(f"progress: {done}/{total} (ok={ok}, fail={fail})")

        typer.echo(f"=== DONE: {ep} === ok={ok}, fail={fail}, total={total}")

    elapsed = time.perf_counter() - t0
    typer.echo(f"\n⏱ elapsed time: {format_elapsed(elapsed)}")


def pretty(x: object) -> str:
    return to_pretty_json(x)
