# Copyright (c) 2023 - 2025, AG2ai, Inc., AG2ai open-source projects maintainers and core contributors
#
# SPDX-License-Identifier: Apache-2.0

from collections.abc import Callable
from typing import Any, Literal
from uuid import UUID

from pydantic import BaseModel

from .base_event import BaseEvent, resolve_print_callable, wrap_event

__all__ = ["UsageSummaryEvent"]


class ModelUsageSummary(BaseModel):
    """Model usage summary."""

    model: str
    """Model name."""
    completion_tokens: int
    """Number of tokens used for completion."""
    cost: float
    """Cost of the completion."""
    prompt_tokens: int
    """Number of tokens used for prompt."""
    total_tokens: int
    """Total number of tokens used."""


class ActualUsageSummary(BaseModel):
    """Actual usage summary."""

    usages: list[ModelUsageSummary] | None = None
    """List of model usage summaries."""
    total_cost: float | None = None
    """Total cost."""


class TotalUsageSummary(BaseModel):
    """Total usage summary."""

    usages: list[ModelUsageSummary] | None = None
    """List of model usage summaries."""
    total_cost: float | None = None
    """Total cost."""


Mode = Literal["both", "total", "actual"]


def _change_usage_summary_format(
    actual_usage_summary: dict[str, Any] | None = None, total_usage_summary: dict[str, Any] | None = None
) -> dict[str, dict[str, Any]]:
    summary: dict[str, Any] = {}

    for usage_type, usage_summary in {"actual": actual_usage_summary, "total": total_usage_summary}.items():
        if usage_summary is None:
            summary[usage_type] = {"usages": None, "total_cost": None}
            continue

        usage_summary_altered_format: dict[str, list[dict[str, Any]]] = {"usages": []}
        for k, v in usage_summary.items():
            if isinstance(k, str) and isinstance(v, dict):
                current_usage = dict(v.items())
                current_usage["model"] = k
                usage_summary_altered_format["usages"].append(current_usage)
            else:
                usage_summary_altered_format[k] = v
        summary[usage_type] = usage_summary_altered_format

    return summary


@wrap_event
class UsageSummaryEvent(BaseEvent):
    """Usage summary message."""

    actual: ActualUsageSummary
    """Actual usage summary."""
    total: TotalUsageSummary
    """Total usage summary."""
    mode: Mode
    """Mode to display the usage summary."""

    def __init__(
        self,
        *,
        uuid: UUID | None = None,
        actual_usage_summary: dict[str, Any] | None = None,
        total_usage_summary: dict[str, Any] | None = None,
        mode: Mode = "both",
    ):
        # print(f"{actual_usage_summary=}")
        # print(f"{total_usage_summary=}")

        summary_dict = _change_usage_summary_format(actual_usage_summary, total_usage_summary)

        super().__init__(uuid=uuid, **summary_dict, mode=mode)

    def _print_usage(
        self,
        usage_summary: ActualUsageSummary | TotalUsageSummary,
        usage_type: str = "total",
        f: Callable[..., Any] | None = None,
    ) -> None:
        f = resolve_print_callable(f)
        word_from_type = "including" if usage_type == "total" else "excluding"
        if usage_summary.usages is None or len(usage_summary.usages) == 0:
            f("No actual cost incurred (all completions are using cache).", flush=True)
            return

        f(f"Usage summary {word_from_type} cached usage: ", flush=True)
        f(f"Total cost: {round(usage_summary.total_cost, 5)}", flush=True)  # type: ignore [arg-type]

        for usage in usage_summary.usages:
            f(
                f"* Model '{usage.model}': cost: {round(usage.cost, 5)}, prompt_tokens: {usage.prompt_tokens}, completion_tokens: {usage.completion_tokens}, total_tokens: {usage.total_tokens}",
                flush=True,
            )

    def print(self, f: Callable[..., Any] | None = None) -> None:
        f = resolve_print_callable(f)

        if self.total.usages is None:
            f('No usage summary. Please call "create" first.', flush=True)
            return

        f("-" * 100, flush=True)
        if self.mode == "both":
            self._print_usage(self.actual, "actual", f)
            f()
            if self.total.model_dump_json() != self.actual.model_dump_json():
                self._print_usage(self.total, "total", f)
            else:
                f(
                    "All completions are non-cached: the total cost with cached completions is the same as actual cost.",
                    flush=True,
                )
        elif self.mode == "total":
            self._print_usage(self.total, "total", f)
        elif self.mode == "actual":
            self._print_usage(self.actual, "actual", f)
        else:
            raise ValueError(f'Invalid mode: {self.mode}, choose from "actual", "total", ["actual", "total"]')
        f("-" * 100, flush=True)


@wrap_event
class StreamEvent(BaseEvent):
    """Stream event."""

    content: str
    """Content of the event."""

    def __init__(self, *, uuid: UUID | None = None, content: str) -> None:
        super().__init__(uuid=uuid, content=content)

    def print(self, f: Callable[..., Any] | None = None) -> None:
        f = resolve_print_callable(f)

        # Set the terminal text color to green
        f("\033[32m", end="")

        f(self.content, end="", flush=True)

        # Reset the terminal text color
        f("\033[0m\n")
