"""CapitalProfile dataclass + estimator dispatch.

Estimator versioning rule (per ticket #000020 §6 — risks):

- ``ESTIMATOR_VERSION`` is the global pin. Bump it when the
  estimator function set changes. Old rows keep their version
  recorded so retroactive re-estimation can be rebuilt cleanly.
- Per-op-type estimators are registered in :data:`ESTIMATORS`;
  unknown op types fall through to a zero-profile + the version
  pin.

Cost-unit conventions (no cross-form conversion at this layer):

- ``living`` — person-minutes equivalent (operator + downstream user
  attention).
- ``material`` — kWh equivalent for compute + storage (rough).
- ``financial`` — USD equivalent for paid resources (e.g., LLM API
  spend).
- ``intellectual`` — net contribution (positive when op produces
  reusable knowledge artifacts; negative when op consumes them
  without preservation).
- ``experiential`` — operator wait-time minutes.
- ``social`` — trust delta on a ±1 named scale.
- ``cultural`` — analogous ±1.
- ``spiritual`` — analogous ±1.
"""

from __future__ import annotations

from dataclasses import asdict, dataclass, field, replace
from typing import Callable, Optional


ESTIMATOR_VERSION = "estimator-v1"


@dataclass(frozen=True)
class CapitalProfile:
    living: float = 0.0
    material: float = 0.0
    financial: float = 0.0
    intellectual: float = 0.0
    experiential: float = 0.0
    social: float = 0.0
    cultural: float = 0.0
    spiritual: float = 0.0

    def as_dict(self) -> dict:
        return asdict(self)

    @classmethod
    def zero(cls) -> "CapitalProfile":
        return cls()


# ---------------------------------------------------------------------
# Estimator dispatch
#
# Each estimator returns a (CapitalProfile, inputs_dict) pair. The
# inputs_dict goes to capital_ledger.estimator_inputs_blob so retroactive
# re-estimation can be reproduced exactly.
# ---------------------------------------------------------------------

Estimator = Callable[[dict], tuple[CapitalProfile, dict]]


def _estimate_ingest(args: dict) -> tuple[CapitalProfile, dict]:
    """Cost of ingesting documents.

    Inputs (all optional; missing keys default to 0):
    - ``doc_count`` — number of documents ingested
    - ``total_bytes`` — uncompressed corpus size

    Heuristic constants (initial v1 — refine empirically):
    - 1e-6 kWh per ingested byte (storage write + index)
    - 1e-9 person-minutes per byte (sysadmin tax, amortized)
    - +0.001 intellectual per doc (positive: knowledge preserved)
    """
    doc_count = float(args.get("doc_count", 0))
    total_bytes = float(args.get("total_bytes", 0))
    inputs = {"doc_count": doc_count, "total_bytes": total_bytes}
    return (
        CapitalProfile(
            material=total_bytes * 1e-6,
            living=total_bytes * 1e-9,
            intellectual=doc_count * 0.001,
        ),
        inputs,
    )


def _estimate_qa(args: dict) -> tuple[CapitalProfile, dict]:
    """Cost of answering one query.

    Inputs:
    - ``prompt_chars`` — total prompt size in chars
    - ``answer_chars`` — answer size in chars
    - ``llm_seconds`` — wall-clock seconds spent in LLM call
    - ``cache_hit`` — bool; cache hits are nearly free

    Heuristics (LLM dominates):
    - Cache hit: nearly zero except 0.01 experiential (operator wait)
    - LLM call: 1e-3 kWh/sec (small model on commodity GPU)
    - LLM call: $1e-5 / 1k chars (rough self-hosted equivalent)
    - Answer chars: -0.0001 intellectual per char (consuming context)
    """
    cache_hit = bool(args.get("cache_hit", False))
    prompt_chars = float(args.get("prompt_chars", 0))
    answer_chars = float(args.get("answer_chars", 0))
    llm_seconds = float(args.get("llm_seconds", 0.0))
    inputs = {
        "cache_hit": cache_hit,
        "prompt_chars": prompt_chars,
        "answer_chars": answer_chars,
        "llm_seconds": llm_seconds,
    }
    if cache_hit:
        return CapitalProfile(experiential=0.01), inputs
    return (
        CapitalProfile(
            material=llm_seconds * 1e-3,
            financial=(prompt_chars + answer_chars) * 1e-5 / 1000.0,
            experiential=llm_seconds / 60.0,
            intellectual=-answer_chars * 0.0001,
        ),
        inputs,
    )


def _estimate_distill(args: dict) -> tuple[CapitalProfile, dict]:
    """Cost of distilling surface documents into core documents.

    Inputs:
    - ``surface_count`` — number of surface docs distilled
    - ``core_count`` — number of core docs produced
    - ``llm_seconds`` — wall-clock seconds spent in LLM call (if any)

    Distillation produces reusable knowledge artifacts (cores), so
    intellectual capital is positive.
    """
    surface_count = float(args.get("surface_count", 0))
    core_count = float(args.get("core_count", 0))
    llm_seconds = float(args.get("llm_seconds", 0.0))
    inputs = {
        "surface_count": surface_count,
        "core_count": core_count,
        "llm_seconds": llm_seconds,
    }
    return (
        CapitalProfile(
            material=llm_seconds * 1e-3,
            experiential=llm_seconds / 60.0,
            intellectual=core_count * 0.005,
        ),
        inputs,
    )


def _estimate_default(args: dict) -> tuple[CapitalProfile, dict]:
    """Fallback: zero profile + the inputs dict for record-keeping."""
    return CapitalProfile.zero(), dict(args)


ESTIMATORS: dict[str, Estimator] = {
    "ingest": _estimate_ingest,
    "qa": _estimate_qa,
    "distill": _estimate_distill,
}


def profile_for_op(
    op_type: str, inputs: Optional[dict] = None
) -> tuple[CapitalProfile, dict]:
    """Dispatch to the right estimator; fallback to zero profile.

    Returns ``(profile, recorded_inputs)``. The recorded inputs are
    written to ``capital_ledger.estimator_inputs_blob`` for replay.
    """
    inputs = dict(inputs or {})
    estimator = ESTIMATORS.get(op_type, _estimate_default)
    return estimator(inputs)
