defmodule Symbiont.Ledger do
  @moduledoc """
  Append-only JSONL ledger for tracking all inference calls.

  Every call to Claude gets logged with model, tokens, cost, timing.
  This is the source of truth for cost tracking and billing analysis.
  """
  use GenServer

  require Logger

  # -- Client API --

  def start_link(opts) do
    GenServer.start_link(__MODULE__, opts, name: __MODULE__)
  end

  @doc "Append a ledger entry. Returns :ok."
  def append(entry) when is_map(entry) do
    GenServer.cast(__MODULE__, {:append, entry})
  end

  @doc "Read the last `n` ledger entries."
  def recent(n \\ 50) do
    GenServer.call(__MODULE__, {:recent, n})
  end

  @doc "Compute aggregate stats across the full ledger."
  def stats do
    GenServer.call(__MODULE__, :stats)
  end

  # -- Server Callbacks --

  @impl true
  def init(opts) do
    data_dir = Keyword.fetch!(opts, :data_dir)
    path = Path.join(data_dir, "ledger.jsonl")

    unless File.exists?(path), do: File.write!(path, "")

    {:ok, %{path: path}}
  end

  @impl true
  def handle_cast({:append, entry}, state) do
    entry_with_ts =
      entry
      |> Map.put_new(:timestamp, DateTime.utc_now() |> DateTime.to_iso8601())

    line = Jason.encode!(entry_with_ts) <> "\n"
    File.write!(state.path, line, [:append])
    Logger.info("Ledger entry: model=#{entry[:model]} cost=$#{entry[:estimated_cost_usd]}")
    {:noreply, state}
  end

  @impl true
  def handle_call({:recent, n}, _from, state) do
    entries =
      state.path
      |> File.stream!()
      |> Stream.reject(&(&1 == "\n"))
      |> Enum.to_list()
      |> Enum.take(-n)
      |> Enum.map(&Jason.decode!/1)

    {:reply, entries, state}
  end

  @impl true
  def handle_call(:stats, _from, state) do
    entries =
      state.path
      |> File.stream!()
      |> Stream.reject(&(&1 == "\n"))
      |> Enum.map(&Jason.decode!/1)

    total_calls = length(entries)
    total_cost = sum_costs(entries)

    by_model =
      entries
      |> Enum.group_by(& &1["model"])
      |> Map.new(fn {model, group} ->
        {model, %{"calls" => length(group), "cost" => sum_costs(group)}}
      end)

    by_date =
      entries
      |> Enum.group_by(fn e ->
        e["timestamp"] |> String.slice(0, 10)
      end)
      |> Map.new(fn {date, group} ->
        {date, %{"calls" => length(group), "cost" => sum_costs(group)}}
      end)

    result = %{
      "total_calls" => total_calls,
      "total_cost_estimated_usd" => total_cost,
      "by_model" => by_model,
      "by_date" => by_date
    }

    {:reply, result, state}
  end

  # -- Private --

  defp sum_costs(entries) do
    entries
    |> Enum.reduce(0.0, fn entry, acc ->
      acc + to_float(entry["estimated_cost_usd"])
    end)
    |> Float.round(4)
  end

  defp to_float(nil), do: 0.0
  defp to_float(n) when is_float(n), do: n
  defp to_float(n) when is_integer(n), do: n * 1.0
  defp to_float(_), do: 0.0
end
