defmodule Mv.Membership.Import.ImportRunner do
  @moduledoc """
  Orchestrates CSV member import: file reading, progress tracking, chunk processing,
  and error formatting. Used by `MvWeb.ImportLive` to keep LiveView thin.

  This module does not depend on Phoenix or LiveView. It provides pure functions for
  progress/merge and side-effectful helpers (read_file_entry, process_chunk) that
  are called from the LiveView or from tasks started by it.
  """

  use Gettext, backend: MvWeb.Gettext

  alias Mv.Membership.Import.MemberCSV

  @default_max_errors 50

  @doc """
  Reads file content from a Phoenix LiveView upload entry (path).

  Used as the callback for `consume_uploaded_entries/3`. Returns `{:ok, content}` or
  `{:error, reason}` with a user-friendly string.
  """
  @spec read_file_entry(map(), map()) :: {:ok, String.t()} | {:error, String.t()}
  def read_file_entry(%{path: path}, _entry) do
    case File.read(path) do
      {:ok, content} ->
        {:ok, content}

      {:error, reason} when is_atom(reason) ->
        {:error, :file.format_error(reason)}

      {:error, %File.Error{reason: reason}} ->
        {:error, :file.format_error(reason)}

      {:error, reason} ->
        {:error, Exception.message(reason)}
    end
  end

  @doc """
  Normalizes the result of `consume_uploaded_entries/3` into `{:ok, content}` or `{:error, reason}`.

  Handles both the standard `[{:ok, content}]` and test helpers that may return `[content]`.
  """
  @spec parse_consume_result(list()) :: {:ok, String.t()} | {:error, String.t()}
  def parse_consume_result(raw) do
    case raw do
      [{:ok, content}] when is_binary(content) -> {:ok, content}
      [content] when is_binary(content) -> {:ok, content}
      [{:error, reason}] -> {:error, gettext("Failed to read file: %{reason}", reason: reason)}
      [] -> {:error, gettext("No file was uploaded")}
      _other -> {:error, gettext("Failed to read uploaded file: unexpected format")}
    end
  end

  @doc """
  Builds the initial progress map from a prepared import_state.
  """
  @spec initial_progress(map(), keyword()) :: map()
  def initial_progress(import_state, opts \\ []) do
    _max_errors = Keyword.get(opts, :max_errors, @default_max_errors)
    total = length(import_state.chunks)

    %{
      inserted: 0,
      failed: 0,
      errors: [],
      warnings: import_state.warnings || [],
      status: :running,
      current_chunk: 0,
      total_chunks: total,
      errors_truncated?: false
    }
  end

  @doc """
  Merges a chunk result into the current progress and returns updated progress.

  Caps errors at `max_errors` (default 50). Sets `status` to `:done` when all chunks
  have been processed.
  """
  @spec merge_progress(map(), map(), non_neg_integer(), keyword()) :: map()
  def merge_progress(progress, chunk_result, current_chunk_idx, opts \\ []) do
    max_errors = Keyword.get(opts, :max_errors, @default_max_errors)

    all_errors = progress.errors ++ chunk_result.errors
    new_errors = Enum.take(all_errors, max_errors)
    errors_truncated? = length(all_errors) > max_errors
    new_warnings = progress.warnings ++ Map.get(chunk_result, :warnings, [])

    chunks_processed = current_chunk_idx + 1
    new_status = if chunks_processed >= progress.total_chunks, do: :done, else: :running

    %{
      inserted: progress.inserted + chunk_result.inserted,
      failed: progress.failed + chunk_result.failed,
      errors: new_errors,
      warnings: new_warnings,
      status: new_status,
      current_chunk: chunks_processed,
      total_chunks: progress.total_chunks,
      errors_truncated?: errors_truncated? || Map.get(chunk_result, :errors_truncated?, false)
    }
  end

  @doc """
  Returns the next action after processing a chunk: send the next chunk index or done.
  """
  @spec next_chunk_action(non_neg_integer(), non_neg_integer()) ::
          {:send_chunk, non_neg_integer()} | :done
  def next_chunk_action(current_idx, total_chunks) do
    next_idx = current_idx + 1
    if next_idx < total_chunks, do: {:send_chunk, next_idx}, else: :done
  end

  @doc """
  Processes one chunk (validate + create members), then sends `{:chunk_done, idx, result}`
  or `{:chunk_error, idx, reason}` to `live_view_pid`.

  Options: `:custom_field_lookup`, `:existing_error_count`, `:max_errors`, `:actor`.
  """
  @spec process_chunk(
          list(),
          map(),
          map(),
          keyword(),
          pid(),
          non_neg_integer()
        ) :: :ok
  def process_chunk(chunk, column_map, custom_field_map, opts, live_view_pid, idx) do
    result =
      try do
        MemberCSV.process_chunk(chunk, column_map, custom_field_map, opts)
      rescue
        e -> {:error, Exception.message(e)}
      catch
        :exit, reason -> {:error, inspect(reason)}
        :throw, reason -> {:error, inspect(reason)}
      end

    case result do
      {:ok, chunk_result} -> send(live_view_pid, {:chunk_done, idx, chunk_result})
      {:error, reason} -> send(live_view_pid, {:chunk_error, idx, reason})
    end

    :ok
  end

  @doc """
  Returns a user-facing error message for chunk failures (invalid index, missing state,
  or processing failure).
  """
  @spec format_chunk_error(
          :invalid_index | :missing_state | :processing_failed,
          non_neg_integer(),
          any()
        ) ::
          String.t()
  def format_chunk_error(:invalid_index, idx, _reason) do
    gettext("Invalid chunk index: %{idx}", idx: idx)
  end

  def format_chunk_error(:missing_state, idx, _reason) do
    gettext("Import state is missing. Cannot process chunk %{idx}.", idx: idx)
  end

  def format_chunk_error(:processing_failed, idx, reason) do
    gettext("Failed to process chunk %{idx}: %{reason}", idx: idx, reason: inspect(reason))
  end
end
