from __future__ import annotations

import logging
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FuturesTimeout
from typing import Any, Callable, TypeVar

from niannian.storage.local import LocalMemoryStore
from niannian.storage.types import PullResult

logger = logging.getLogger(__name__)

T = TypeVar("T")

_DEFAULT_FRESH_TIMEOUT_SEC = 8.0


def _read_cfg(config: dict[str, Any] | None) -> dict[str, Any]:
    return (config or {}).get("sync") or {}


def _fresh_timeout_sec(config: dict[str, Any] | None) -> float:
    read_cfg = _read_cfg(config).get("read") or {}
    raw = read_cfg.get("ensure_fresh_timeout_sec", _DEFAULT_FRESH_TIMEOUT_SEC)
    try:
        return max(1.0, float(raw))
    except (TypeError, ValueError):
        return _DEFAULT_FRESH_TIMEOUT_SEC


def _run_bounded(timeout_sec: float, fn: Callable[..., T], /, *args: Any, **kwargs: Any) -> T | None:
    """云 I/O 限时执行；超时/失败返回 None，不阻断 Chat。"""
    with ThreadPoolExecutor(max_workers=1) as ex:
        fut = ex.submit(fn, *args, **kwargs)
        try:
            return fut.result(timeout=timeout_sec)
        except FuturesTimeout:
            logger.debug("%s timed out after %.1fs", getattr(fn, "__name__", fn), timeout_sec)
            return None
        except Exception as exc:
            logger.debug("%s failed: %s", getattr(fn, "__name__", fn), exc)
            return None


def _boot_timeout_sec(config: dict[str, Any] | None) -> float:
    read_cfg = _read_cfg(config).get("read") or {}
    raw = read_cfg.get("boot_hydrate_timeout_sec", 30.0)
    try:
        return max(1.0, float(raw))
    except (TypeError, ValueError):
        return 30.0


def boot_hydrate(
    store: LocalMemoryStore,
    *,
    config: dict[str, Any] | None = None,
) -> PullResult:
    """启动时从 L1 拉取到 L0；cloud 未配置则跳过。"""
    cfg = _read_cfg(config)
    read_cfg = cfg.get("read") or {}
    if not read_cfg.get("boot_hydrate", True):
        return PullResult(ok=True, errors=["boot_hydrate disabled in config"])

    cloud = store.cloud
    if not cloud.cloud_ready():
        return PullResult(ok=True, errors=["cloud not configured — hydrate skipped"])

    try:
        timeout = _boot_timeout_sec(config)
        result = _run_bounded(timeout, store.pull_from_cloud)
        if result is None:
            logger.warning("boot_hydrate pull timed out after %.1fs", timeout)
            return PullResult(ok=False, errors=[f"boot_hydrate timed out after {timeout:.0f}s"])
        if not result.ok:
            logger.warning("boot_hydrate pull incomplete: %s", result.errors)
        return result
    except Exception as exc:
        logger.warning("boot_hydrate failed: %s", exc)
        return PullResult(ok=False, errors=[str(exc)])


def ensure_fresh_for_read(
    store: LocalMemoryStore,
    *,
    config: dict[str, Any] | None = None,
    max_staleness_sec: int = 0,
) -> PullResult | None:
    """读前校验：remote manifest version > local 时 pull；失败不阻断 Chat。"""
    _ = max_staleness_sec  # reserved for future TTL-based refresh
    cfg = _read_cfg(config)
    read_cfg = cfg.get("read") or {}
    if not read_cfg.get("ensure_fresh_before_recall", True):
        return None

    cloud = store.cloud
    if not cloud.cloud_ready():
        return None

    timeout = _fresh_timeout_sec(config)
    try:
        remote = _run_bounded(timeout, cloud.read_remote_manifest)
        if remote is None:
            return None
        local_version = store._manifest.read().version
        if remote.version <= local_version:
            return None
        result = _run_bounded(timeout, store.pull_from_cloud)
        if result is None:
            logger.debug("ensure_fresh_for_read pull timed out or failed")
            return None
        if not result.ok:
            logger.debug("ensure_fresh_for_read pull incomplete: %s", result.errors)
        return result
    except Exception as exc:
        logger.debug("ensure_fresh_for_read skipped: %s", exc)
        return None
