defmodule Raxol.ACP.Job.Server do
  @moduledoc """
  GenServer holding the in-flight state for a single ACP job.

  One process per active job, registered by job ID via
  `Raxol.ACP.Job.Registry`. Two layers of API:

  - **Low-level** -- `transition/4` accepts event + payload + opaque
    signature. Validates against `Raxol.ACP.Job.StateMachine`, creates
    an on-chain memo via `Raxol.ACP.ContractClient`, appends to memo
    history, emits telemetry.

  - **Orchestration** -- `accept_request/1`, `deliver/1`,
    `accept_payment/3`, `approve/3`. Active when the server is
    configured with a `:handler` (any module implementing
    `Raxol.ACP.Offering.Handler`). The orchestration helpers invoke
    the handler at the right state and fire the next transition.

  ## Signatures

  The `signature` argument on `transition/4`, `accept_payment/3`, and
  `approve/3` is the **buyer's** authorization (e.g. ERC-3009 for
  payments) and is kept in the local memo log for record-keeping. It
  is NOT sent on-chain by `createMemo` -- the canonical ACP contract
  does not accept a separate memo signature; the transaction itself
  is signed by the calling wallet.

  Terminates with `:normal` on a transition into a terminal state
  (`:completed` or `:expired`). Combined with the transient restart in
  `Raxol.ACP.Job.Supervisor`, completed jobs do not resurrect.

  ## Telemetry

  Emits `[:raxol, :acp, :job, :transition]` on every successful
  transition with metadata
  `%{job_id, from, to, memo_type, tx_hash}`.
  """

  use Raxol.Core.Behaviours.BaseManager

  alias Raxol.ACP.ContractClient
  alias Raxol.ACP.Job.{MemoType, Registry, StateMachine, Store}

  @type memo :: %{
          next_phase: StateMachine.state(),
          memo_type: MemoType.t(),
          content: String.t(),
          is_secured: boolean(),
          payload: map() | nil,
          signature: binary() | nil,
          tx_hash: ContractClient.tx_hash(),
          transitioned_at: DateTime.t()
        }

  @type config :: %{
          optional(:handler) => module(),
          optional(:request) => map(),
          optional(:buyer) => String.t(),
          optional(:seller) => String.t()
        }

  @type t :: %__MODULE__{
          job_id: ContractClient.job_id(),
          state: StateMachine.state(),
          memos: [memo()],
          config: config(),
          persist?: boolean()
        }

  defstruct [:job_id, :state, memos: [], config: %{}, persist?: true]

  # -- Public API --

  @doc """
  Start a Job.Server registered under the given job ID.

  ## Required options

  - `:job_id` -- the ACP job id (binary or integer).

  ## Optional state options

  - `:initial_state` -- defaults to `StateMachine.initial/0`.

  ## Optional orchestration options

  These enable the high-level helpers (`accept_request/1`, `deliver/1`,
  `accept_payment/3`, `approve/3`). Without them, only `transition/4`
  works.

  - `:handler` -- module implementing `Raxol.ACP.Offering.Handler`.
  - `:request` -- the buyer's request map; passed to handler callbacks.
  - `:buyer` -- buyer address (0x string), surfaced in handler ctx.
  - `:seller` -- seller address (0x string), surfaced in handler ctx.

  ## Optional persistence options

  - `:persist?` -- default `true`. When true, every successful
    transition writes through `Raxol.ACP.Job.Store`, and `init/1`
    hydrates state + memos from the store if a prior record exists for
    this `:job_id`. Set to `false` to bypass persistence (e.g. tests
    that exercise raw transition semantics).
  """
  @spec start_link(keyword()) :: GenServer.on_start()
  def start_link(opts) do
    job_id = Keyword.fetch!(opts, :job_id)
    GenServer.start_link(__MODULE__, opts, name: Registry.via(job_id))
  end

  @doc """
  Drive the state machine forward by `event` with a caller-supplied
  payload and signature. Low-level entry point used by both
  orchestration helpers and tests that want to bypass the handler.
  """
  @spec transition(GenServer.server() | binary(), StateMachine.event(), map(), binary()) ::
          {:ok, StateMachine.state()} | {:error, term()}
  def transition(server, event, payload, signature) do
    GenServer.call(resolve(server), {:transition, event, payload, signature})
  end

  @doc """
  Invoke the configured handler's `handle_request/2` callback. On
  `{:accept, response}`, fire `:accept_request` (advancing to
  `:negotiation`). On `{:reject, reason}`, fire `:expire`.

  Requires `:handler` and `:request` in the server's config.
  """
  @spec accept_request(GenServer.server() | binary()) ::
          {:ok, StateMachine.state()} | {:error, term()}
  def accept_request(server) do
    GenServer.call(resolve(server), :accept_request)
  end

  @doc """
  Buyer-side: record the buyer's payment authorization and advance to
  `:transaction`.

  If `payload` is supplied, it becomes the memo content. `signature`
  is the buyer's authorization (e.g. ERC-3009); stored in the local
  memo log for record-keeping but not sent on-chain.
  """
  @spec accept_payment(GenServer.server() | binary(), map(), binary() | nil) ::
          {:ok, StateMachine.state()} | {:error, term()}
  def accept_payment(server, payload, signature \\ nil) do
    GenServer.call(resolve(server), {:accept_payment, payload, signature})
  end

  @doc """
  Invoke the configured handler's `handle_deliver/2` callback. On
  `{:deliver, deliverable}`, fire `:deliver` (advancing to
  `:evaluation`). On `{:error, reason}`, fire `:expire`.
  """
  @spec deliver(GenServer.server() | binary()) ::
          {:ok, StateMachine.state()} | {:error, term()}
  def deliver(server) do
    GenServer.call(resolve(server), :deliver)
  end

  @doc """
  Buyer/evaluator-side: approve the deliverable and finalize the job.

  Same payload/signature semantics as `accept_payment/3`.
  """
  @spec approve(GenServer.server() | binary(), map(), binary() | nil) ::
          {:ok, StateMachine.state()} | {:error, term()}
  def approve(server, payload, signature \\ nil) do
    GenServer.call(resolve(server), {:approve, payload, signature})
  end

  @doc "Return the full Job.Server struct for inspection."
  @spec get_state(GenServer.server() | binary()) :: t()
  def get_state(server), do: GenServer.call(resolve(server), :get_state)

  @doc "Return the current StateMachine state."
  @spec current_state(GenServer.server() | binary()) :: StateMachine.state()
  def current_state(server), do: GenServer.call(resolve(server), :current_state)

  @doc "Return the memo history in submission order."
  @spec memos(GenServer.server() | binary()) :: [memo()]
  def memos(server), do: GenServer.call(resolve(server), :memos)

  defp resolve(pid) when is_pid(pid), do: pid
  defp resolve(name) when is_atom(name), do: name
  defp resolve(job_id) when is_binary(job_id), do: Registry.via(job_id)
  defp resolve({:via, _, _} = via), do: via

  # -- GenServer callbacks --

  @impl Raxol.Core.Behaviours.BaseManager
  def init_manager(opts) do
    config =
      opts
      |> Keyword.take([:handler, :request, :buyer, :seller])
      |> Map.new()

    job_id = Keyword.fetch!(opts, :job_id)
    persist? = Keyword.get(opts, :persist?, true)
    initial = Keyword.get(opts, :initial_state, StateMachine.initial())
    {state, memos} = hydrate(job_id, persist?, initial)

    {:ok,
     %__MODULE__{
       job_id: job_id,
       state: state,
       memos: memos,
       config: config,
       persist?: persist?
     }}
  end

  # Restore prior state + memos from the Store on a transient restart.
  # If persistence is disabled or no record exists, fall back to the
  # caller-supplied initial state with no memo history.
  defp hydrate(job_id, true, initial) do
    case Store.load(job_id) do
      {:ok, %{state: state, memos: memos}} -> {state, memos}
      :error -> {initial, []}
    end
  end

  defp hydrate(_job_id, false, initial), do: {initial, []}

  @impl Raxol.Core.Behaviours.BaseManager
  def handle_manager_call({:transition, event, payload, signature}, _from, state) do
    do_transition(state, event, payload, signature)
  end

  def handle_manager_call(:accept_request, _from, state) do
    with {:ok, %{handler: handler, request: request}} <- need(state.config, [:handler, :request]) do
      case handler.handle_request(request, ctx(state)) do
        {:accept, response} ->
          do_transition(state, :accept_request, response, nil)

        {:reject, reason} ->
          do_transition(state, :expire, %{reason: inspect(reason)}, nil)
      end
    else
      {:error, _} = err -> {:reply, err, state}
    end
  end

  def handle_manager_call({:accept_payment, payload, signature}, _from, state) do
    do_transition(state, :accept_payment, payload, signature)
  end

  def handle_manager_call(:deliver, _from, state) do
    with {:ok, %{handler: handler, request: request}} <- need(state.config, [:handler, :request]) do
      case handler.handle_deliver(request, ctx(state)) do
        {:deliver, deliverable} ->
          do_transition(state, :deliver, deliverable, nil)

        {:error, reason} ->
          do_transition(state, :expire, %{reason: inspect(reason)}, nil)
      end
    else
      {:error, _} = err -> {:reply, err, state}
    end
  end

  def handle_manager_call({:approve, payload, signature}, _from, state) do
    do_transition(state, :approve, payload, signature)
  end

  def handle_manager_call(:get_state, _from, state), do: {:reply, state, state}
  def handle_manager_call(:current_state, _from, state), do: {:reply, state.state, state}
  def handle_manager_call(:memos, _from, state), do: {:reply, state.memos, state}

  # -- Private --

  defp do_transition(state, event, payload, signature) do
    memo_type = memo_type_for_event(event)
    content = encode_content(payload)

    with {:ok, new_state} <- StateMachine.next(state.state, event),
         {:ok, tx_hash} <-
           ContractClient.create_memo(state.job_id, content, memo_type, false, new_state) do
      memo = %{
        next_phase: new_state,
        memo_type: memo_type,
        content: content,
        is_secured: false,
        payload: ensure_payload(payload),
        signature: signature,
        tx_hash: tx_hash,
        transitioned_at: DateTime.utc_now()
      }

      :telemetry.execute(
        [:raxol, :acp, :job, :transition],
        %{},
        %{
          job_id: state.job_id,
          from: state.state,
          to: new_state,
          memo_type: memo_type,
          next_phase: new_state,
          tx_hash: tx_hash
        }
      )

      new_full_state = %{state | state: new_state, memos: state.memos ++ [memo]}
      maybe_persist(new_full_state, memo)

      if StateMachine.terminal?(new_state) do
        {:stop, :normal, {:ok, new_state}, new_full_state}
      else
        {:reply, {:ok, new_state}, new_full_state}
      end
    else
      {:error, reason} -> {:reply, {:error, reason}, state}
    end
  end

  # Mirror every successful transition to the Store so a transient
  # restart can pick up where the prior process left off. No-op when
  # persistence is disabled.
  defp maybe_persist(%{persist?: false}, _memo), do: :ok

  defp maybe_persist(%{persist?: true, job_id: job_id, state: state}, memo) do
    Store.append_memo(job_id, state, memo)
  end

  # Default memo type per event. Payment/approval steps reference a
  # transaction hash; everything else is a free-form message. Callers
  # who need a different `MemoType` (e.g. an image_url for a deliverable)
  # should call ContractClient.create_memo/5 directly.
  defp memo_type_for_event(:accept_payment), do: :txhash
  defp memo_type_for_event(:approve), do: :txhash
  defp memo_type_for_event(_), do: :message

  defp encode_content(nil), do: ""
  defp encode_content(payload) when is_binary(payload), do: payload
  defp encode_content(payload) when is_map(payload), do: Jason.encode!(payload)

  defp ensure_payload(payload) when is_map(payload), do: payload
  defp ensure_payload(_), do: nil

  defp ctx(state) do
    %{
      job_id: state.job_id,
      buyer: Map.get(state.config, :buyer),
      seller: Map.get(state.config, :seller),
      state: state.state
    }
  end

  defp need(config, keys) do
    case Enum.reject(keys, &Map.has_key?(config, &1)) do
      [] -> {:ok, Map.take(config, keys)}
      missing -> {:error, {:config_missing, missing}}
    end
  end
end
