defmodule Microwaveprop.Weather.Grib2.Section do
  @moduledoc false

  @doc """
  Parse a single GRIB2 message binary into its component sections.

  Returns `{:ok, %{grid_params, product, packing_params, data, bitmap}}`.
  """
  @spec parse_message(binary()) ::
          {:ok,
           %{
             grid_params: map(),
             product: %{var: String.t(), level: String.t()},
             packing_params: map(),
             data: binary(),
             bitmap: :none | :present
           }}
          | {:error, String.t()}
  def parse_message(<<"GRIB", _reserved::16, _discipline::8, 2::8, total_length::64-big, rest::binary>> = msg) do
    discipline = :binary.at(msg, 6)
    # Skip indicator section (16 bytes), parse remaining sections
    message_body = binary_part(rest, 0, total_length - 16)
    parse_sections(message_body, %{discipline: discipline})
  end

  def parse_message(<<"GRIB", _reserved::16, _discipline::8, edition::8, _::binary>>) do
    {:error, "unsupported GRIB edition #{edition}"}
  end

  def parse_message(_), do: {:error, "not a GRIB2 message"}

  @doc """
  Identify variable name from discipline, parameter category, and parameter number.
  """
  @spec identify_var(non_neg_integer(), non_neg_integer(), non_neg_integer()) :: String.t()
  def identify_var(0, 0, 0), do: "TMP"
  def identify_var(0, 0, 6), do: "DPT"
  def identify_var(0, 3, 0), do: "PRES"
  def identify_var(0, 3, 5), do: "HGT"
  def identify_var(0, 3, 18), do: "HPBL"
  def identify_var(0, 1, 0), do: "SPFH"
  def identify_var(0, 1, 3), do: "PWAT"
  def identify_var(0, 1, 8), do: "APCP"
  def identify_var(0, 2, 2), do: "UGRD"
  def identify_var(0, 2, 3), do: "VGRD"
  def identify_var(0, 6, 1), do: "TCDC"
  def identify_var(0, 19, 11), do: "TKE"
  def identify_var(_discipline, cat, num), do: "#{cat}:#{num}"

  @doc """
  Identify level string from surface type and scaled value.
  """
  @spec identify_level(non_neg_integer(), non_neg_integer()) :: String.t()
  def identify_level(1, _value), do: "surface"
  def identify_level(100, value_pa), do: "#{div(value_pa, 100)} mb"
  def identify_level(103, value_m), do: "#{value_m} m above ground"

  def identify_level(10, _value), do: "entire atmosphere"
  def identify_level(105, level), do: "#{level} hybrid level"
  def identify_level(200, _value), do: "entire atmosphere (considered as a single layer)"

  def identify_level(type, value), do: "unknown:#{type}:#{value}"

  @doc """
  Decode a 16-bit sign-magnitude integer.
  """
  @spec sign_magnitude_16(<<_::16>>) :: integer()
  def sign_magnitude_16(<<0::1, value::15>>), do: value
  def sign_magnitude_16(<<1::1, value::15>>), do: -value

  # --- Private section parsing ---

  defp parse_sections(<<"7777", _rest::binary>>, acc) do
    build_result(acc)
  end

  defp parse_sections(<<length::32-big, section_num::8, rest::binary>>, acc) when length > 5 do
    body_length = length - 5

    if body_length <= byte_size(rest) do
      <<body::binary-size(body_length), remaining::binary>> = rest
      acc = parse_section(section_num, body, acc)
      parse_sections(remaining, acc)
    else
      # Truncated section — return what we have
      build_result(acc)
    end
  end

  defp parse_sections(<<>>, acc) do
    build_result(acc)
  end

  # Trailing padding or short garbage — return what we have
  defp parse_sections(_other, acc) do
    build_result(acc)
  end

  defp parse_section(1, _body, acc) do
    # Section 1 (Identification) — we don't need anything from it
    acc
  end

  defp parse_section(3, body, acc) do
    <<
      _source::8,
      _num_points::32-big,
      _optional_octets::8,
      _interpretation::8,
      template::16-big,
      rest::binary
    >> = body

    grid_params = parse_grid_template(template, rest)
    Map.put(acc, :grid_params, grid_params)
  end

  defp parse_section(4, body, acc) do
    <<
      _num_coords::16-big,
      _template::16-big,
      param_category::8,
      param_number::8,
      _generating_process::8,
      _background::8,
      _forecast_process::8,
      _cutoff_hours::16-big,
      _cutoff_minutes::8,
      _time_unit::8,
      _forecast_time::32-big,
      first_surface_type::8,
      _first_scale_factor::8,
      first_scaled_value::32-big,
      _rest::binary
    >> = body

    discipline = Map.get(acc, :discipline, 0)
    var = identify_var(discipline, param_category, param_number)
    level = identify_level(first_surface_type, first_scaled_value)

    Map.put(acc, :product, %{var: var, level: level})
  end

  defp parse_section(5, body, acc) do
    <<
      num_data_points::32-big,
      template::16-big,
      ref_value_bytes::binary-size(4),
      binary_scale_bytes::binary-size(2),
      decimal_scale_bytes::binary-size(2),
      bits_per_value::8,
      rest::binary
    >> = body

    <<ref_value::float-32-big>> = ref_value_bytes

    base_params = %{
      num_data_points: num_data_points,
      template: template,
      reference_value: ref_value,
      binary_scale: sign_magnitude_16(binary_scale_bytes),
      decimal_scale: sign_magnitude_16(decimal_scale_bytes),
      bits_per_value: bits_per_value
    }

    packing_params =
      case template do
        0 -> base_params
        3 -> parse_complex_packing_params(base_params, rest)
        _ -> base_params
      end

    Map.put(acc, :packing_params, packing_params)
  end

  defp parse_section(6, <<indicator::8, _rest::binary>>, acc) do
    bitmap =
      if indicator == 255 do
        :none
      else
        :present
      end

    Map.put(acc, :bitmap, bitmap)
  end

  defp parse_section(7, data, acc) do
    Map.put(acc, :data, data)
  end

  defp parse_section(_section_num, _body, acc) do
    # Skip unknown sections (Section 2, etc.)
    acc
  end

  defp parse_complex_packing_params(base, rest) do
    <<
      _original_type::8,
      _splitting_method::8,
      missing_mgmt::8,
      _primary_missing::32-big,
      _secondary_missing::32-big,
      num_groups::32-big,
      ref_group_widths::8,
      nbits_group_widths::8,
      ref_group_lengths::32-big,
      length_increment::8,
      last_group_length::32-big,
      nbits_group_lengths::8,
      spatial_order::8,
      num_extra_octets::8,
      _extra::binary
    >> = rest

    Map.merge(base, %{
      missing_mgmt: missing_mgmt,
      num_groups: num_groups,
      ref_group_widths: ref_group_widths,
      nbits_group_widths: nbits_group_widths,
      ref_group_lengths: ref_group_lengths,
      length_increment: length_increment,
      last_group_length: last_group_length,
      nbits_group_lengths: nbits_group_lengths,
      spatial_order: spatial_order,
      num_extra_octets: num_extra_octets
    })
  end

  # Template 3.30 — Lambert Conformal Conic
  defp parse_grid_template(30, body) do
    <<
      _shape::8,
      _scale_factor_radius::8,
      _scaled_radius::32-big,
      _scale_factor_major::8,
      _scaled_major::32-big,
      _scale_factor_minor::8,
      _scaled_minor::32-big,
      nx::32-big,
      ny::32-big,
      la1_micro::32-big,
      lo1_micro::32-big,
      _resolution::8,
      lad_micro::32-big,
      lov_micro::32-big,
      dx_mm::32-big,
      dy_mm::32-big,
      _projection_centre::8,
      scan_mode::8,
      latin1_micro::32-big,
      latin2_micro::32-big,
      _lat_sp::32-big,
      _lon_sp::32-big,
      _rest::binary
    >> = body

    %{
      nx: nx,
      ny: ny,
      la1: la1_micro / 1_000_000,
      lo1: lo1_micro / 1_000_000,
      lad: lad_micro / 1_000_000,
      lov: lov_micro / 1_000_000,
      dx: dx_mm / 1000,
      dy: dy_mm / 1000,
      latin1: latin1_micro / 1_000_000,
      latin2: latin2_micro / 1_000_000,
      scan_mode: scan_mode
    }
  end

  defp parse_grid_template(template, _body) do
    %{error: "unsupported grid template #{template}"}
  end

  defp build_result(acc) do
    required = [:grid_params, :product, :packing_params, :data, :bitmap]

    if Enum.all?(required, &Map.has_key?(acc, &1)) do
      {:ok, Map.take(acc, required)}
    else
      missing = Enum.reject(required, &Map.has_key?(acc, &1))
      {:error, "missing sections: #{inspect(missing)}"}
    end
  end
end
