from __future__ import annotations

import json
from datetime import datetime, timezone
from typing import Any

from niannian.storage.sync.conditions import parse_iso

CLEAN_BUNDLE_FORMAT = "niannian.archive/clean-v1"
FORBIDDEN_BUNDLE_KEYS = frozenset({"raw_text", "body"})


def effective_age_threshold(count_at_7d: int, atom_age_days: float, cfg: dict[str, Any]) -> bool:
    """§4.1 归档年龄门槛：7 天 + 不足 10 条延后；14 天强制。"""
    base = float(cfg.get("min_age_days", 7))
    extra = float(cfg.get("defer_extra_days", 7))
    minimum = int(cfg.get("min_distilled_atoms", 50))
    if atom_age_days < base:
        return False
    if atom_age_days >= base + extra:
        return True
    if count_at_7d >= minimum:
        return atom_age_days >= base
    return False


def dream_age_days(dream_at: str, now: datetime) -> float | None:
    ts = parse_iso(dream_at)
    if ts is None:
        return None
    delta = now - ts.astimezone(timezone.utc)
    return delta.total_seconds() / 86400.0


def make_snippet(text: str, max_len: int = 80) -> str:
    cleaned = " ".join(str(text).split())
    if len(cleaned) <= max_len:
        return cleaned
    return cleaned[: max_len - 1] + "…"


def _atom_kind(atom: Any) -> str:
    meta = getattr(atom, "meta", None) or {}
    if isinstance(meta, dict):
        taxonomy = meta.get("taxonomy") or {}
        if isinstance(taxonomy, dict) and taxonomy.get("kind"):
            return str(taxonomy["kind"])
        if meta.get("kind"):
            return str(meta["kind"])
    return "note"


def build_atom_index_entry(atom: Any, lifecycle: dict[str, Any]) -> dict[str, Any]:
    lc = lifecycle.get(atom.id, {})
    dream_at = lc.get("dream_at") or lc.get("updated_at") or atom.created_at
    return {
        "id": atom.id,
        "kind": _atom_kind(atom),
        "tags": list(atom.tags or []),
        "project": atom.project,
        "dream_at": dream_at,
        "snippet": make_snippet(atom.raw_text),
    }


def collect_summaries_for_tags(summaries: dict[str, Any], tags: set[str]) -> dict[str, Any]:
    out: dict[str, Any] = {}
    for tag in sorted(tags):
        item = summaries.get(tag)
        if not isinstance(item, dict):
            continue
        out[tag] = {
            "text": str(item.get("text", "")),
            "status": str(item.get("status", "distilled")),
        }
    return out


def collect_wiki_refs(wiki_pages: dict[str, Any], atom_ids: set[str], tags: set[str]) -> list[dict[str, str]]:
    refs: list[dict[str, str]] = []
    for page_id, page in wiki_pages.items():
        if not isinstance(page, dict):
            continue
        title = str(page.get("title") or page.get("concept") or page_id)
        linked_ids = set(page.get("atom_ids") or [])
        page_tags = set(page.get("tags") or [])
        if linked_ids & atom_ids or page_tags & tags:
            refs.append({"id": str(page_id), "title": title})
    return refs


def build_clean_bundle(
    *,
    atoms: list[Any],
    lifecycle: dict[str, Any],
    summaries: dict[str, Any],
    wiki_pages: dict[str, Any],
    project: str,
    now: datetime | None = None,
) -> dict[str, Any]:
    now = now or datetime.now(timezone.utc)
    tags: set[str] = set()
    atom_ids: set[str] = set()
    index: list[dict[str, Any]] = []
    for atom in atoms:
        atom_ids.add(atom.id)
        tags.update(atom.tags or [])
        index.append(build_atom_index_entry(atom, lifecycle))

    bundle: dict[str, Any] = {
        "format": CLEAN_BUNDLE_FORMAT,
        "created_at": now.astimezone(timezone.utc).isoformat(),
        "project": project,
        "summaries": collect_summaries_for_tags(summaries, tags),
        "atom_index": index,
        "wiki_refs": collect_wiki_refs(wiki_pages, atom_ids, tags),
        "stats": {"atoms_indexed": len(index), "bytes": 0},
    }
    payload = json.dumps(bundle, ensure_ascii=False)
    bundle["stats"]["bytes"] = len(payload.encode("utf-8"))
    return bundle


def bundle_contains_forbidden_fields(obj: Any) -> bool:
    if isinstance(obj, dict):
        for key, value in obj.items():
            if key in FORBIDDEN_BUNDLE_KEYS:
                return True
            if key == "content" and isinstance(value, dict) and "body" in value:
                return True
            if bundle_contains_forbidden_fields(value):
                return True
        return False
    if isinstance(obj, list):
        return any(bundle_contains_forbidden_fields(item) for item in obj)
    return False


def select_distilled_candidates(
    atoms: list[Any],
    lifecycle: dict[str, Any],
    *,
    require_dream: bool,
    tiers: set[str],
) -> list[Any]:
    out: list[Any] = []
    for atom in atoms:
        lc = lifecycle.get(atom.id, {})
        status = lc.get("status", atom.status)
        if status not in tiers:
            continue
        if lc.get("nas_archived"):
            continue
        if require_dream and not lc.get("dream_at"):
            continue
        out.append(atom)
    return out


def select_archive_batch(
    candidates: list[Any],
    lifecycle: dict[str, Any],
    cfg: dict[str, Any],
    now: datetime,
) -> tuple[list[Any], dict[str, Any]]:
    """返回纳入本次 bundle 的 atoms 与 defer 元数据。"""
    base = float(cfg.get("min_age_days", 7))
    minimum = int(cfg.get("min_distilled_atoms", 50))
    extra = float(cfg.get("defer_extra_days", 7))

    aged: list[tuple[Any, float]] = []
    for atom in candidates:
        lc = lifecycle.get(atom.id, {})
        dream_at = lc.get("dream_at")
        if not dream_at:
            continue
        age = dream_age_days(dream_at, now)
        if age is None:
            continue
        aged.append((atom, age))

    count_at_7d = sum(1 for _, age in aged if age >= base)
    selected: list[Any] = []
    for atom, age in aged:
        if effective_age_threshold(count_at_7d, age, cfg):
            selected.append(atom)

    meta: dict[str, Any] = {
        "eligible_at_7d": count_at_7d,
        "min_distilled_atoms": minimum,
    }
    if not selected and count_at_7d > 0 and count_at_7d < minimum:
        max_age = max(age for _, age in aged)
        if max_age < base + extra:
            meta["skipped_reason"] = "below_min_atoms_deferred"
            meta["next_retry_after_days"] = int(extra)
    return selected, meta
