"""Token estimation without an API call.

`ablate cost` promises `$0`, no network, no API key (docs/06-cost.md), which
rules out the provider's own token-counting endpoint. It also rules out
shipping a tokenizer: `tiktoken` is OpenAI's vocabulary, not Anthropic's, so
it would look precise while being wrong for the model actually under test, and
ADR-0001 keeps the dependency list short as a standing constraint.

So this is an **estimate**, and every surface that prints it says so. That is a
deliberate trade, and it is a smaller compromise than it first appears:

- The absolute number is good to roughly ±10-15% on English-plus-code markdown.
- The *ratio* between two sections of similar prose is far more reliable than
  either absolute figure, and ratios are what section attribution needs.
- docs/05-statistics.md calls the config's own token cost the one metric with
  "zero variance." Zero *variance* is right — this function is deterministic,
  and re-running it never moves the number. It is not zero *bias*, and the
  reports do not claim it is.
"""

from __future__ import annotations

import re

#: Characters per token for ordinary English prose. The usual rule of thumb,
#: and close enough that docs/05's worked example (a 400-line CLAUDE.md at
#: ~4,000 tokens) falls out of it.
CHARS_PER_TOKEN_PROSE = 4.2

#: Code tokenizes denser than prose: identifiers split, punctuation and
#: indentation each cost a token, and little of it is in the merged vocabulary.
CHARS_PER_TOKEN_CODE = 3.1

#: Stated accuracy of the estimate, surfaced wherever the number is printed so
#: nobody builds a budget on a false precision.
ACCURACY = 0.15

_FENCE = re.compile(r"^\s*```", re.MULTILINE)


def _split_fenced(text: str) -> tuple[str, str]:
    """Partition into (prose, fenced code). Unterminated fences count as code."""
    prose_parts: list[str] = []
    code_parts: list[str] = []
    in_code = False
    for line in text.splitlines(keepends=True):
        if _FENCE.match(line):
            # The fence line itself is markup; bill it to whichever side it
            # opens or closes, it is a rounding error either way.
            (code_parts if in_code else prose_parts).append(line)
            in_code = not in_code
            continue
        (code_parts if in_code else prose_parts).append(line)
    return "".join(prose_parts), "".join(code_parts)


def estimate_tokens(text: str | None) -> int:
    """Deterministic token estimate for a chunk of config text.

    Prose and fenced code are counted at different densities, because a
    `CLAUDE.md` that is mostly command examples tokenizes appreciably heavier
    than one that is mostly guidance, and the difference is large enough to
    change which section looks expensive.
    """
    if not text:
        return 0
    prose, code = _split_fenced(text)
    return round(len(prose) / CHARS_PER_TOKEN_PROSE + len(code) / CHARS_PER_TOKEN_CODE)


def format_estimate(tokens: int) -> str:
    """Render with a tilde, so the number never reads as exact."""
    return f"~{tokens:,}"
