defmodule Symbiont.Queue do
  @moduledoc """
  Persistent task queue backed by JSONL file.

  Tasks flow through states: pending → processing → done | failed
  The queue is durable — survives restarts via the JSONL file.
  """
  use GenServer

  require Logger

  defstruct [:path, tasks: []]

  # -- Client API --

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

  @doc "Add a task to the queue. Returns the task ID."
  def enqueue(task_text, priority \\ "normal") do
    GenServer.call(__MODULE__, {:enqueue, task_text, priority})
  end

  @doc "Take up to `n` pending tasks for processing."
  def take(n \\ 1) do
    GenServer.call(__MODULE__, {:take, n})
  end

  @doc "Mark a task as completed."
  def complete(task_id, result \\ nil) do
    GenServer.cast(__MODULE__, {:complete, task_id, result})
  end

  @doc "Mark a task as failed."
  def fail(task_id, reason \\ nil) do
    GenServer.cast(__MODULE__, {:fail, task_id, reason})
  end

  @doc "Get the current queue size (pending tasks only)."
  def size do
    GenServer.call(__MODULE__, :size)
  end

  @doc "List all tasks (optionally filtered by status)."
  def list(status \\ nil) do
    GenServer.call(__MODULE__, {:list, status})
  end

  # -- Server Callbacks --

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

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

    tasks = load_tasks(path)
    Logger.info("Queue loaded: #{length(tasks)} tasks (#{count_pending(tasks)} pending)")

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

  @impl true
  def handle_call({:enqueue, task_text, priority}, _from, state) do
    task = %{
      "id" => generate_id(),
      "task" => task_text,
      "priority" => priority,
      "status" => "pending",
      "created_at" => DateTime.utc_now() |> DateTime.to_iso8601()
    }

    new_tasks = state.tasks ++ [task]
    persist!(state.path, new_tasks)

    {:reply, {:ok, task["id"]}, %{state | tasks: new_tasks}}
  end

  @impl true
  def handle_call({:take, n}, _from, state) do
    {to_process, rest} =
      state.tasks
      |> Enum.split_with(&(&1["status"] == "pending"))

    taken = Enum.take(to_process, n)
    remaining_pending = Enum.drop(to_process, n)

    updated_taken =
      Enum.map(taken, &Map.put(&1, "status", "processing"))

    new_tasks = updated_taken ++ remaining_pending ++ rest
    persist!(state.path, new_tasks)

    {:reply, updated_taken, %{state | tasks: new_tasks}}
  end

  @impl true
  def handle_call(:size, _from, state) do
    {:reply, count_pending(state.tasks), state}
  end

  @impl true
  def handle_call({:list, nil}, _from, state) do
    {:reply, state.tasks, state}
  end

  @impl true
  def handle_call({:list, status}, _from, state) do
    filtered = Enum.filter(state.tasks, &(&1["status"] == to_string(status)))
    {:reply, filtered, state}
  end

  @impl true
  def handle_cast({:complete, task_id, result}, state) do
    new_tasks = update_task_status(state.tasks, task_id, "done", result)
    persist!(state.path, new_tasks)
    {:noreply, %{state | tasks: new_tasks}}
  end

  @impl true
  def handle_cast({:fail, task_id, reason}, state) do
    new_tasks = update_task_status(state.tasks, task_id, "failed", reason)
    persist!(state.path, new_tasks)
    {:noreply, %{state | tasks: new_tasks}}
  end

  # -- Private --

  defp load_tasks(path) do
    path
    |> File.stream!()
    |> Stream.reject(&(&1 in ["", "\n"]))
    |> Enum.map(fn line ->
      case Jason.decode(String.trim(line)) do
        {:ok, task} -> task
        {:error, _} -> nil
      end
    end)
    |> Enum.reject(&is_nil/1)
  end

  defp persist!(path, tasks) do
    content =
      tasks
      |> Enum.map(&(Jason.encode!(&1) <> "\n"))
      |> Enum.join()

    File.write!(path, content)
  end

  defp update_task_status(tasks, task_id, status, extra) do
    Enum.map(tasks, fn task ->
      if task["id"] == task_id do
        task
        |> Map.put("status", status)
        |> Map.put("completed_at", DateTime.utc_now() |> DateTime.to_iso8601())
        |> then(fn t ->
          if extra, do: Map.put(t, "result", extra), else: t
        end)
      else
        task
      end
    end)
  end

  defp count_pending(tasks) do
    Enum.count(tasks, &(&1["status"] == "pending"))
  end

  defp generate_id do
    ts = System.system_time(:second)
    rand = :rand.uniform(9999)
    "task-#{ts}-#{rand}"
  end
end
