"""
Practice: measure two API discounts the way Lab 12's scripts 03 and 04 do. Prompt caching bills
the repeated start of a prompt at a tenth of the price (on gpt-5.4-nano), but only up to the first
difference, and only for prompts of about 1,024 tokens or more. The Batch API charges half price
for work that can wait, and its results must be matched by custom_id. You'll price a call, count
cache hits, find how much of a prompt can be reused, and match and compare a batch's results.

Solution.
"""

# gpt-5.4-nano list prices, $ per million tokens (Lab 12's inference_utils.PRICES)
PRICES = {"input": 0.20, "cached_input": 0.02, "output": 1.25}
BATCH_DISCOUNT = 0.5    # the Batch API charges half the list price
MIN_CACHE_TOKENS = 1024  # OpenAI's docs: prompts shorter than about this aren't cached


def call_cost(input_tokens: int, cached_tokens: int, output_tokens: int) -> float:
    """Dollar cost of one call. cached_tokens is the part of input_tokens read from the cache
    (usage.prompt_tokens_details.cached_tokens): billed at the cached price, the rest at the input price."""
    uncached = input_tokens - cached_tokens
    return (uncached * PRICES["input"] + cached_tokens * PRICES["cached_input"]
            + output_tokens * PRICES["output"]) / 1_000_000


def hits_after_first(runs: list[list[int]]) -> tuple[int, int]:
    """runs: the cached_tokens of each call, one list per run (5 calls each). The first call of a run
    always starts cold, so count only calls 2 onwards. Return (calls that hit, calls counted)."""
    later = [c for run in runs for c in run[1:]]
    return sum(1 for c in later if c > 0), len(later)


def reusable_start(previous: str, new: str) -> int:
    """How many characters at the start of `new` are identical to the start of `previous`?
    The cache can only reuse a prompt's start, never past the first difference."""
    n = 0
    for a, b in zip(previous, new):
        if a != b:
            break
        n += 1
    return n


def can_cache(prompt_tokens: int) -> bool:
    """Is the prompt long enough to be cached at all?"""
    return prompt_tokens >= MIN_CACHE_TOKENS


def match_by_id(results: list[dict]) -> dict:
    """results: the batch's output lines, in whatever order they came back. Each is
    {"custom_id": ..., "answer": ...}. Return {custom_id: answer}. Never rely on position."""
    return {r["custom_id"]: r["answer"] for r in results}


def missing_ids(sent: list[str], results: list[dict]) -> list[str]:
    """The custom_ids that were sent but have no result: the ones to resubmit. Keep the sent order."""
    got = {r["custom_id"] for r in results}
    return [i for i in sent if i not in got]


def batch_cost(input_tokens: int, output_tokens: int) -> float:
    """Dollar cost of a batch's tokens at the batch price (half the list price; no caching here)."""
    return call_cost(input_tokens, 0, output_tokens) * BATCH_DISCOUNT


def agreement(a: list[str], b: list[str]) -> int:
    """How many positions hold the same category in both lists? (Both lists are already in ticket order.)"""
    return sum(1 for x, y in zip(a, b) if x == y)
