This file is a merged representation of the entire codebase, combined into a single document by Repomix.
The content has been processed where content has been compressed (code blocks are separated by ⋮---- delimiter).

<file_summary>
This section contains a summary of this file.

<purpose>
This file contains a packed representation of the entire repository's contents.
It is designed to be easily consumable by AI systems for analysis, code review,
or other automated processes.
</purpose>

<file_format>
The content is organized as follows:
1. This summary section
2. Repository information
3. Directory structure
4. Repository files (if enabled)
5. Multiple file entries, each consisting of:
  - File path as an attribute
  - Full contents of the file
</file_format>

<usage_guidelines>
- This file should be treated as read-only. Any changes should be made to the
  original repository files, not this packed version.
- When processing this file, use the file path to distinguish
  between different files in the repository.
- Be aware that this file may contain sensitive information. Handle it with
  the same level of security as you would the original repository.
</usage_guidelines>

<notes>
- Some files may have been excluded based on .gitignore rules and Repomix's configuration
- Binary files are not included in this packed representation. Please refer to the Repository Structure section for a complete list of file paths, including binary files
- Files matching patterns in .gitignore are excluded
- Files matching default ignore patterns are excluded
- Content has been compressed - code blocks are separated by ⋮---- delimiter
- Files are sorted by Git change count (files with more changes are at the bottom)
</notes>

</file_summary>

<directory_structure>
json_remedy/
  context/
    context_values.ex
    json_context.ex
  layer1/
    content_cleaning.ex
  layer2/
    structural_repair.ex
  layer3/
    syntax_normalization.ex
  utils/
    char_utils.ex
  layer_behaviour.ex
</directory_structure>

<files>
This section contains the contents of the repository's files.

<file path="json_remedy/context/context_values.ex">
defmodule JsonRemedy.Context.ContextValues do
  @moduledoc """
  Enum definitions and utilities for JSON parsing context values.

  This module provides validation, transition logic, and repair prioritization
  for context values used throughout the JSON repair pipeline.
  """

  alias JsonRemedy.Context.JsonContext

  @valid_contexts [:root, :object_key, :object_value, :array]

  @doc """
  Returns all valid context values.

  ## Examples

      iex> JsonRemedy.Context.ContextValues.valid_context_values()
      [:root, :object_key, :object_value, :array]

  """
  @spec valid_context_values() :: [JsonContext.context_value()]
  def valid_context_values do
    @valid_contexts
  end

  @doc """
  Checks if a context value is valid.

  ## Examples

      iex> JsonRemedy.Context.ContextValues.is_valid_context?(:object_key)
      true

      iex> JsonRemedy.Context.ContextValues.is_valid_context?(:invalid)
      false

  """
  @spec is_valid_context?(any()) :: boolean()
  def is_valid_context?(context) do
    context in @valid_contexts
  end

  @doc """
  Determines if a transition between contexts is valid.

  ## Examples

      iex> JsonRemedy.Context.ContextValues.can_transition_to?(:root, :object_key)
      true

      iex> JsonRemedy.Context.ContextValues.can_transition_to?(:root, :object_value)
      false

  """
  @spec can_transition_to?(any(), any()) :: boolean()
  def can_transition_to?(from, to) do
    case {from, to} do
      # Invalid contexts
      {from, _to} when from not in @valid_contexts -> false
      {_from, to} when to not in @valid_contexts -> false
      # Valid transitions from root
      {:root, :object_key} -> true
      {:root, :array} -> true
      {:root, _} -> false
      # Valid transitions from object_key
      {:object_key, :object_value} -> true
      {:object_key, :object_key} -> true
      {:object_key, :array} -> true
      {:object_key, _} -> false
      # Valid transitions from object_value
      {:object_value, :object_key} -> true
      {:object_value, :object_value} -> true
      {:object_value, :array} -> true
      {:object_value, _} -> false
      # Valid transitions from array
      {:array, :object_key} -> true
      {:array, :array} -> true
      {:array, :object_value} -> true
      {:array, _} -> false
    end
  end

  @doc """
  Predicts the next expected context based on current context and character.

  ## Examples

      iex> JsonRemedy.Context.ContextValues.next_expected_context(:object_key, ":")
      :object_value

      iex> JsonRemedy.Context.ContextValues.next_expected_context(:object_value, ",")
      :object_key

  """
  @spec next_expected_context(JsonContext.context_value(), String.t()) ::
          JsonContext.context_value() | :pop_context
  def next_expected_context(:object_key, ":"), do: :object_value
  def next_expected_context(:object_key, "="), do: :object_value
  def next_expected_context(:object_key, ","), do: :object_key
  def next_expected_context(:object_key, _), do: :object_key

  def next_expected_context(:object_value, ","), do: :object_key
  def next_expected_context(:object_value, "}"), do: :pop_context
  def next_expected_context(:object_value, _), do: :object_value

  def next_expected_context(:array, ","), do: :array
  def next_expected_context(:array, "]"), do: :pop_context
  def next_expected_context(:array, _), do: :array

  def next_expected_context(:root, _), do: :root

  @doc """
  Determines if a context allows a specific type of repair.

  ## Examples

      iex> JsonRemedy.Context.ContextValues.context_allows_repair?(:object_key, :unquoted_keys)
      true

      iex> JsonRemedy.Context.ContextValues.context_allows_repair?(:object_key, :boolean_normalization)
      false

  """
  @spec context_allows_repair?(JsonContext.context_value(), atom()) :: boolean()
  def context_allows_repair?(:object_key, repair_type) do
    repair_type in [:quote_normalization, :unquoted_keys, :colon_fix]
  end

  def context_allows_repair?(:object_value, repair_type) do
    repair_type in [:boolean_normalization, :null_normalization, :quote_normalization, :comma_fix]
  end

  def context_allows_repair?(:array, repair_type) do
    repair_type in [
      :comma_fix,
      :bracket_fix,
      :boolean_normalization,
      :null_normalization,
      :quote_normalization
    ]
  end

  def context_allows_repair?(:root, repair_type) do
    repair_type in [:brace_fix, :bracket_fix, :comment_removal, :structure_repair]
  end

  def context_allows_repair?(_, _), do: false

  @doc """
  Returns the priority for a repair type in a given context.

  Higher numbers indicate higher priority.

  ## Examples

      iex> JsonRemedy.Context.ContextValues.get_repair_priority(:object_key, :unquoted_keys)
      80

      iex> JsonRemedy.Context.ContextValues.get_repair_priority(:invalid_context, :quote_normalization)
      50

  """
  @spec get_repair_priority(JsonContext.context_value(), atom()) :: non_neg_integer()
  def get_repair_priority(:object_key, repair_type) do
    case repair_type do
      :unquoted_keys -> 80
      :quote_normalization -> 70
      :colon_fix -> 60
      _ -> 50
    end
  end

  def get_repair_priority(:object_value, repair_type) do
    case repair_type do
      :boolean_normalization -> 80
      :null_normalization -> 75
      :quote_normalization -> 70
      :comma_fix -> 60
      _ -> 50
    end
  end

  def get_repair_priority(:array, repair_type) do
    case repair_type do
      :bracket_fix -> 80
      :comma_fix -> 70
      :boolean_normalization -> 60
      :null_normalization -> 60
      :quote_normalization -> 50
      _ -> 50
    end
  end

  def get_repair_priority(:root, repair_type) do
    case repair_type do
      :structure_repair -> 90
      :brace_fix -> 80
      :bracket_fix -> 80
      :comment_removal -> 70
      _ -> 50
    end
  end

  def get_repair_priority(_, _), do: 50
end
</file>

<file path="json_remedy/context/json_context.ex">
defmodule JsonRemedy.Context.JsonContext do
  @moduledoc """
  Centralized context tracking for JSON repair operations.

  This module provides state management for tracking the current parsing context,
  which enables context-aware repair decisions across all layers. It tracks
  whether we're in an object key, object value, array, or string context.
  """

  @type context_value :: :root | :object_key | :object_value | :array

  @type t :: %__MODULE__{
          current: context_value(),
          stack: [context_value()],
          position: non_neg_integer(),
          in_string: boolean(),
          string_delimiter: String.t() | nil
        }

  defstruct current: :root,
            stack: [],
            position: 0,
            in_string: false,
            string_delimiter: nil

  @doc """
  Creates a new empty context with default values.

  ## Examples

      iex> context = JsonRemedy.Context.JsonContext.new()
      iex> context.current
      :root
      iex> context.stack
      []
      iex> context.position
      0
      iex> context.in_string
      false

  """
  @spec new() :: %__MODULE__{
          current: :root,
          stack: [],
          position: 0,
          in_string: false,
          string_delimiter: nil
        }
  def new do
    %__MODULE__{}
  end

  @doc """
  Pushes a new context onto the context stack.

  The current context becomes the new context and the previous
  current context is pushed onto the stack.

  ## Examples

      iex> context = JsonRemedy.Context.JsonContext.new()
      iex> new_context = JsonRemedy.Context.JsonContext.push_context(context, :object_key)
      iex> new_context.current
      :object_key
      iex> new_context.stack
      [:root]

  """
  @spec push_context(t(), context_value()) :: t()
  def push_context(%__MODULE__{} = context, new_context) do
    %{context | current: new_context, stack: [context.current | context.stack]}
  end

  @doc """
  Pops the most recent context from the stack.

  If the stack is empty, the context remains unchanged.

  ## Examples

      iex> context = %JsonRemedy.Context.JsonContext{current: :object_key, stack: [:root]}
      iex> popped = JsonRemedy.Context.JsonContext.pop_context(context)
      iex> popped.current
      :root
      iex> popped.stack
      []

  """
  @spec pop_context(t()) :: t()
  def pop_context(%__MODULE__{stack: []} = context) do
    context
  end

  def pop_context(%__MODULE__{stack: [head | tail]} = context) do
    %{context | current: head, stack: tail}
  end

  @doc """
  Enters string parsing context with the given delimiter.

  ## Examples

      iex> context = JsonRemedy.Context.JsonContext.new()
      iex> string_context = JsonRemedy.Context.JsonContext.enter_string(context, "\\"")
      iex> string_context.in_string
      true
      iex> string_context.string_delimiter
      "\\""

  """
  @spec enter_string(t(), String.t()) :: t()
  def enter_string(%__MODULE__{} = context, delimiter) do
    %{context | in_string: true, string_delimiter: delimiter}
  end

  @doc """
  Exits string parsing context.

  ## Examples

      iex> context = %JsonRemedy.Context.JsonContext{in_string: true, string_delimiter: "\\""}
      iex> exited = JsonRemedy.Context.JsonContext.exit_string(context)
      iex> exited.in_string
      false
      iex> exited.string_delimiter
      nil

  """
  @spec exit_string(t()) :: t()
  def exit_string(%__MODULE__{} = context) do
    %{context | in_string: false, string_delimiter: nil}
  end

  @doc """
  Updates the current position in the input string.

  ## Examples

      iex> context = JsonRemedy.Context.JsonContext.new()
      iex> updated = JsonRemedy.Context.JsonContext.update_position(context, 15)
      iex> updated.position
      15

  """
  @spec update_position(t(), non_neg_integer()) :: t()
  def update_position(%__MODULE__{} = context, position) do
    %{context | position: position}
  end

  @doc """
  Transitions from the current context to a new context.

  Unlike push_context/2, this doesn't modify the stack.

  ## Examples

      iex> context = %JsonRemedy.Context.JsonContext{current: :object_key}
      iex> transitioned = JsonRemedy.Context.JsonContext.transition_context(context, :object_value)
      iex> transitioned.current
      :object_value

  """
  @spec transition_context(t(), context_value()) :: t()
  def transition_context(%__MODULE__{} = context, new_context) do
    %{context | current: new_context}
  end

  @doc """
  Checks if the context is currently inside a string.

  ## Examples

      iex> context = JsonRemedy.Context.JsonContext.new()
      iex> JsonRemedy.Context.JsonContext.is_in_string?(context)
      false

      iex> string_context = JsonRemedy.Context.JsonContext.enter_string(context, "\\"")
      iex> JsonRemedy.Context.JsonContext.is_in_string?(string_context)
      true

  """
  @spec is_in_string?(t()) :: boolean()
  def is_in_string?(%__MODULE__{in_string: in_string}) do
    in_string
  end

  @doc """
  Returns the depth of the context stack.

  ## Examples

      iex> context = JsonRemedy.Context.JsonContext.new()
      iex> JsonRemedy.Context.JsonContext.context_stack_depth(context)
      0

      iex> nested = %JsonRemedy.Context.JsonContext{stack: [:root, :array]}
      iex> JsonRemedy.Context.JsonContext.context_stack_depth(nested)
      2

  """
  @spec context_stack_depth(t()) :: non_neg_integer()
  def context_stack_depth(%__MODULE__{stack: stack}) do
    length(stack)
  end

  @doc """
  Determines if a repair can be applied in the current context.

  String delimiter repairs are allowed when in strings, but most other
  repairs should be avoided to prevent corrupting string content.

  ## Examples

      iex> context = JsonRemedy.Context.JsonContext.new()
      iex> JsonRemedy.Context.JsonContext.can_apply_repair?(context, :quote_normalization)
      true

      iex> string_context = JsonRemedy.Context.JsonContext.enter_string(context, "\\"")
      iex> JsonRemedy.Context.JsonContext.can_apply_repair?(string_context, :quote_normalization)
      false
      iex> JsonRemedy.Context.JsonContext.can_apply_repair?(string_context, :string_delimiter)
      true

  """
  @spec can_apply_repair?(t(), atom()) :: boolean()
  def can_apply_repair?(%__MODULE__{in_string: false}, _repair_type) do
    # All repairs allowed when not in string
    true
  end

  def can_apply_repair?(%__MODULE__{in_string: true}, :string_delimiter) do
    # String delimiter repairs allowed in strings
    true
  end

  def can_apply_repair?(%__MODULE__{in_string: true}, _repair_type) do
    # Most repairs not allowed in strings to prevent corruption
    false
  end
end
</file>

<file path="json_remedy/layer1/content_cleaning.ex">
defmodule JsonRemedy.Layer1.ContentCleaning do
  @moduledoc """
  Layer 1: Content Cleaning - Removes non-JSON content and normalizes encoding.

  This layer handles:
  - Code fence removal (```json ... ```)
  - Comment stripping (// and /* */)
  - Wrapper text extraction (HTML, prose)
  - Encoding normalization

  Uses direct string methods instead of regex for better performance and clearer code.
  """

  @behaviour JsonRemedy.LayerBehaviour

  alias JsonRemedy.LayerBehaviour

  # Import types from LayerBehaviour
  @type repair_action :: LayerBehaviour.repair_action()
  @type repair_context :: LayerBehaviour.repair_context()
  @type layer_result :: LayerBehaviour.layer_result()

  @doc """
  Process input string and apply Layer 1 content cleaning repairs.

  Returns:
  - `{:ok, processed_input, updated_context}` - Layer completed successfully
  - `{:continue, input, context}` - Layer doesn't apply, pass to next layer
  - `{:error, reason}` - Layer failed, stop pipeline
  """
  @spec process(input :: String.t(), context :: repair_context()) :: layer_result()
  def process(input, context) do
    {cleaned_input, new_repairs} =
      input
      |> remove_code_fences()
      |> remove_comments()
      |> extract_json_content_internal()
      |> normalize_encoding_internal()

    updated_context = %{
      repairs: context.repairs ++ new_repairs,
      options: context.options,
      metadata: Map.put(Map.get(context, :metadata, %{}), :layer1_processed, true)
    }

    {:ok, cleaned_input, updated_context}
  rescue
    error ->
      {:error, "Layer 1 Content Cleaning failed: #{inspect(error)}"}
  end

  @doc """
  Remove code fences from input while preserving fence content in strings.
  """
  @spec remove_code_fences(input :: String.t()) :: {String.t(), [repair_action()]}
  def remove_code_fences(input) do
    # Check for code fences using string methods
    if has_code_fences?(input) and not fence_in_string?(input) do
      extract_from_code_fence(input)
    else
      {input, []}
    end
  end

  @doc """
  Strip comments while preserving comment-like content in strings.
  """
  @spec remove_comments(input :: {String.t(), [repair_action()]}) ::
          {String.t(), [repair_action()]}
  def remove_comments({input, existing_repairs}) do
    {result, line_comment_repairs} = remove_line_comments(input)
    {result, block_comment_repairs} = remove_block_comments(result)

    all_repairs = existing_repairs ++ line_comment_repairs ++ block_comment_repairs
    {result, all_repairs}
  end

  @doc """
  Extract JSON from wrapper text (HTML, prose, etc.).
  """
  @spec extract_json_content_internal(input :: {String.t(), [repair_action()]}) ::
          {String.t(), [repair_action()]}
  def extract_json_content_internal({input, existing_repairs}) do
    # Try to extract JSON from HTML tags first
    {result, html_repairs} = extract_from_html_tags(input)

    # Then try to extract from prose/text
    {result, prose_repairs} = extract_from_prose(result)

    all_repairs = existing_repairs ++ html_repairs ++ prose_repairs
    {result, all_repairs}
  end

  @doc """
  Normalize text encoding to UTF-8.
  """
  @spec normalize_encoding_internal(input :: {String.t(), [repair_action()]}) ::
          {String.t(), [repair_action()]}
  def normalize_encoding_internal({input, existing_repairs}) do
    if String.valid?(input) do
      {input, existing_repairs}
    else
      # Remove non-ASCII characters using direct character filtering
      cleaned = filter_to_ascii(input)

      repair = %{
        layer: :content_cleaning,
        action: "normalized encoding",
        position: nil,
        original: nil,
        replacement: nil
      }

      {cleaned, existing_repairs ++ [repair]}
    end
  end

  # LayerBehaviour callback implementations

  @doc """
  Check if this layer can handle the given input.
  Layer 1 can handle any text input that may contain JSON with wrapping content.
  """
  @spec supports?(input :: String.t()) :: boolean()
  def supports?(input) when is_binary(input) do
    # Layer 1 can attempt to process any string input
    # It looks for code fences, comments, or wrapper content
    # Use fast string pattern matching instead of expensive operations
    String.contains?(input, "```") or
      String.contains?(input, "//") or
      String.contains?(input, "/*") or
      String.contains?(input, "<pre>") or
      String.contains?(input, "<code>") or
      long_text_with_content?(input)
  end

  def supports?(_), do: false

  @doc """
  Return the priority order for this layer.
  Layer 1 (Content Cleaning) should run first in the pipeline.
  """
  @spec priority() :: 1
  def priority, do: 1

  @doc """
  Return a human-readable name for this layer.
  """
  @spec name() :: String.t()
  def name, do: "Content Cleaning"

  @doc """
  Validate layer configuration and options.
  Layer 1 accepts options for enabling/disabling specific cleaning features.
  """
  @spec validate_options(options :: keyword()) :: :ok | {:error, String.t()}
  def validate_options(options) when is_list(options) do
    valid_keys = [:remove_comments, :remove_code_fences, :extract_from_html, :normalize_encoding]

    invalid_keys = Keyword.keys(options) -- valid_keys

    if Enum.empty?(invalid_keys) do
      # Validate option values
      case validate_option_values(options) do
        :ok -> :ok
        error -> error
      end
    else
      {:error, "Invalid options: #{inspect(invalid_keys)}. Valid options: #{inspect(valid_keys)}"}
    end
  end

  def validate_options(_), do: {:error, "Options must be a keyword list"}

  defp validate_option_values(options) do
    boolean_options = [
      :remove_comments,
      :remove_code_fences,
      :extract_from_html,
      :normalize_encoding
    ]

    Enum.reduce_while(options, :ok, fn {key, value}, _acc ->
      if key in boolean_options and not is_boolean(value) do
        {:halt, {:error, "Option #{key} must be a boolean, got: #{inspect(value)}"}}
      else
        {:cont, :ok}
      end
    end)
  end

  # Public API functions that match the API contracts

  @doc """
  Strip comments while preserving comment-like content in strings.
  Public API version that takes string input directly.
  """
  @spec strip_comments(input :: String.t()) :: {String.t(), [repair_action()]}
  def strip_comments(input) when is_binary(input) do
    remove_comments({input, []})
  end

  @doc """
  Extract JSON from wrapper text (HTML, prose, etc.).
  Public API version that takes string input directly.
  """
  @spec extract_json_content(input :: String.t()) :: {String.t(), [repair_action()]}
  def extract_json_content(input) when is_binary(input) do
    # Need to rename one of these functions to avoid conflicts
    # For now, call the internal pipeline function directly
    extract_json_content_internal({input, []})
  end

  @doc """
  Normalize text encoding to UTF-8.
  Public API version that takes string input directly.
  """
  @spec normalize_encoding(input :: String.t()) :: {String.t(), [repair_action()]}
  def normalize_encoding(input) when is_binary(input) do
    # Need to rename one of these functions to avoid conflicts
    # For now, call the internal pipeline function directly
    normalize_encoding_internal({input, []})
  end

  # Private helper functions - all using direct string methods instead of regex

  # Code Fence Handling
  defp has_code_fences?(input) do
    String.contains?(input, "```")
  end

  defp fence_in_string?(input) do
    # Check if ``` appears inside a string literal
    parts = String.split(input, "```", parts: 2)

    if length(parts) == 2 do
      [before_fence, _after] = parts
      quote_count = count_unescaped_quotes(before_fence)
      rem(quote_count, 2) != 0
    else
      false
    end
  end

  defp extract_from_code_fence(input) do
    lines = String.split(input, "\n")

    case find_code_fence_boundaries(lines) do
      {start_idx, end_idx} ->
        # Extract content between fences
        content_lines = Enum.slice(lines, (start_idx + 1)..(end_idx - 1))
        content = Enum.join(content_lines, "\n")

        repair = %{
          layer: :content_cleaning,
          action: "removed code fences",
          position: nil,
          original: input,
          replacement: String.trim(content)
        }

        {String.trim(content), [repair]}

      nil ->
        # Malformed fences - try to extract anyway
        extract_malformed_fence_content(input)
    end
  end

  defp find_code_fence_boundaries(lines) do
    start_idx =
      Enum.find_index(lines, fn line ->
        trimmed = String.trim(line)
        starts_with_fence?(trimmed)
      end)

    if start_idx do
      # Look for closing fence after the start
      end_idx =
        Enum.find_index(Enum.drop(lines, start_idx + 1), fn line ->
          trimmed = String.trim(line)
          String.starts_with?(trimmed, "```") or String.starts_with?(trimmed, "``")
        end)

      if end_idx do
        # Adjust index since we dropped elements
        actual_end_idx = start_idx + 1 + end_idx
        {start_idx, actual_end_idx}
      else
        nil
      end
    else
      nil
    end
  end

  defp starts_with_fence?(line) do
    String.starts_with?(line, "```") or String.starts_with?(line, "``")
  end

  defp extract_malformed_fence_content(input) do
    # Handle cases like ```json\n{content} or {content}\n```
    cond do
      # Check for malformed fences like ``json first (before checking ```)
      String.starts_with?(input, "``") and not String.starts_with?(input, "```") ->
        # Handle malformed fences like ``json\n{content}```
        parts = String.split(input, "``")

        if length(parts) >= 2 do
          # Find the part with JSON content
          content_part =
            Enum.find(parts, fn part ->
              trimmed = String.trim(part)
              String.contains?(trimmed, "{") or String.contains?(trimmed, "[")
            end)

          if content_part do
            content = String.trim(content_part)
            # Remove trailing ``` if present
            content = String.replace_suffix(content, "```", "")
            content = remove_language_prefix(content)

            repair = %{
              layer: :content_cleaning,
              action: "removed code fences",
              position: nil,
              original: input,
              replacement: content
            }

            {content, [repair]}
          else
            {input, []}
          end
        else
          {input, []}
        end

      String.contains?(input, "```") ->
        parts = String.split(input, "```")

        if length(parts) >= 2 do
          # Take the middle part that likely contains JSON
          content = Enum.at(parts, 1) || Enum.at(parts, 0)
          content = String.trim(content)

          # Remove language identifiers
          content = remove_language_prefix(content)

          repair = %{
            layer: :content_cleaning,
            action: "removed code fences",
            position: nil,
            original: input,
            replacement: content
          }

          {content, [repair]}
        else
          {input, []}
        end

      true ->
        {input, []}
    end
  end

  defp remove_language_prefix(content) do
    lines = String.split(content, "\n")

    case lines do
      [first_line | rest] ->
        if is_language_line?(first_line) do
          Enum.join(rest, "\n")
        else
          content
        end

      _ ->
        content
    end
  end

  defp is_language_line?(line) do
    trimmed = String.trim(line)
    language_keywords = ["json", "javascript", "js", "JSON"]

    Enum.any?(language_keywords, fn keyword -> String.contains?(trimmed, keyword) end) and
      not String.contains?(trimmed, "{") and not String.contains?(trimmed, "[")
  end

  # Comment Removal using direct string methods
  defp remove_line_comments(input) do
    lines = String.split(input, "\n")

    {processed_lines, repairs} =
      lines
      |> Enum.with_index()
      |> Enum.map_reduce([], fn {line, _index}, acc_repairs ->
        if String.contains?(line, "//") and not line_has_comment_in_string?(line) do
          cleaned_line = remove_line_comment_from_line(line)

          repair = %{
            layer: :content_cleaning,
            action: "removed line comment",
            position: nil,
            original: line,
            replacement: cleaned_line
          }

          {cleaned_line, [repair | acc_repairs]}
        else
          {line, acc_repairs}
        end
      end)

    result = Enum.join(processed_lines, "\n")
    {result, Enum.reverse(repairs)}
  end

  defp remove_line_comment_from_line(line) do
    # Find // that's not inside a string
    case find_line_comment_position(line) do
      nil -> line
      pos -> String.slice(line, 0, pos) |> String.trim_trailing()
    end
  end

  defp find_line_comment_position(line) do
    find_line_comment_position(line, 0, false)
  end

  defp find_line_comment_position(<<>>, _pos, _in_string), do: nil

  defp find_line_comment_position(<<"//", _rest::binary>>, pos, false), do: pos

  defp find_line_comment_position(<<"\"", rest::binary>>, pos, in_string) do
    find_line_comment_position(rest, pos + 1, not in_string)
  end

  defp find_line_comment_position(<<"\\\"", rest::binary>>, pos, in_string) do
    # Skip escaped quote
    find_line_comment_position(rest, pos + 2, in_string)
  end

  defp find_line_comment_position(<<_char::utf8, rest::binary>>, pos, in_string) do
    find_line_comment_position(rest, pos + 1, in_string)
  end

  defp remove_block_comments(input) do
    case find_block_comment(input) do
      nil ->
        {input, []}

      {start_pos, end_pos, comment_text} ->
        if not comment_inside_string?(input, start_pos) do
          before = String.slice(input, 0, start_pos)
          after_comment = String.slice(input, end_pos + 2, String.length(input))
          result = before <> after_comment

          repair = %{
            layer: :content_cleaning,
            action: "removed block comment",
            position: start_pos,
            original: comment_text,
            replacement: ""
          }

          # Recursively remove more block comments
          {final_result, more_repairs} = remove_block_comments(result)
          {final_result, [repair | more_repairs]}
        else
          {input, []}
        end
    end
  end

  defp find_block_comment(input) do
    find_block_comment(input, 0)
  end

  defp find_block_comment(input, start_offset) do
    case find_substring_position(input, "/*", start_offset) do
      nil ->
        nil

      start_pos ->
        # For nested comments, find the matching */ by counting nesting levels
        case find_matching_block_comment_end(input, start_pos + 2) do
          nil ->
            nil

          end_pos ->
            comment_length = end_pos - start_pos + 2
            comment_text = String.slice(input, start_pos, comment_length)
            {start_pos, end_pos, comment_text}
        end
    end
  end

  defp find_matching_block_comment_end(input, start_pos) do
    find_matching_block_comment_end(input, start_pos, 1)
  end

  defp find_matching_block_comment_end(input, pos, nesting_level) when nesting_level > 0 do
    case {find_substring_position(input, "/*", pos), find_substring_position(input, "*/", pos)} do
      {nil, nil} ->
        nil

      {nil, close_pos} ->
        if nesting_level == 1,
          do: close_pos,
          else: find_matching_block_comment_end(input, close_pos + 2, nesting_level - 1)

      {_open_pos, nil} ->
        nil

      {open_pos, close_pos} when open_pos < close_pos ->
        find_matching_block_comment_end(input, open_pos + 2, nesting_level + 1)

      {_open_pos, close_pos} ->
        if nesting_level == 1,
          do: close_pos,
          else: find_matching_block_comment_end(input, close_pos + 2, nesting_level - 1)
    end
  end

  defp find_matching_block_comment_end(_input, _pos, 0), do: nil

  defp find_substring_position(string, substring, start_offset) do
    # Search from the start_offset position
    search_string = String.slice(string, start_offset, String.length(string))

    case String.split(search_string, substring, parts: 2) do
      [before, _after] ->
        start_offset + byte_size(before)

      [_single_part] ->
        nil

      _ ->
        nil
    end
  end

  # HTML Tag Extraction using direct string methods
  defp extract_from_html_tags(input) do
    cond do
      String.contains?(input, "<pre>") and String.contains?(input, "</pre>") ->
        extract_from_tag(input, "<pre>", "</pre>", "extracted JSON from HTML wrapper")

      String.contains?(input, "<code>") and String.contains?(input, "</code>") ->
        extract_from_tag(input, "<code>", "</code>", "extracted JSON from HTML wrapper")

      String.contains?(input, "<json>") and String.contains?(input, "</json>") ->
        extract_from_tag(input, "<json>", "</json>", "extracted JSON from HTML wrapper")

      true ->
        {input, []}
    end
  end

  defp extract_from_tag(input, open_tag, close_tag, action_description) do
    case String.split(input, open_tag, parts: 2) do
      [_before, rest] ->
        case String.split(rest, close_tag, parts: 2) do
          [content, _after] ->
            cleaned_content = String.trim(content)

            repair = %{
              layer: :content_cleaning,
              action: action_description,
              position: nil,
              original: nil,
              replacement: nil
            }

            {cleaned_content, [repair]}

          [_single_part] ->
            {input, []}
        end

      [_single_part] ->
        {input, []}
    end
  end

  # Prose Extraction using direct string methods
  defp extract_from_prose(input) do
    # Simple heuristic: if input contains clear JSON structure
    # but also has a lot of prose, try to extract the JSON part
    if should_extract_from_prose?(input) do
      case extract_json_like_content(input) do
        nil ->
          {input, []}

        json_content ->
          repair = %{
            layer: :content_cleaning,
            action: "extracted JSON from wrapper text",
            position: nil,
            original: input,
            replacement: json_content
          }

          {String.trim(json_content), [repair]}
      end
    else
      {input, []}
    end
  end

  defp should_extract_from_prose?(input) do
    String.length(input) > 100 and
      not String.starts_with?(String.trim(input), "{") and
      not String.starts_with?(String.trim(input), "[") and
      (String.contains?(input, "{") or String.contains?(input, "["))
  end

  defp extract_json_like_content(input) do
    # Look for content that starts with { or [
    cond do
      json_pos = find_json_start(input, "{") ->
        extract_balanced_content(input, json_pos, "{", "}")

      json_pos = find_json_start(input, "[") ->
        extract_balanced_content(input, json_pos, "[", "]")

      true ->
        nil
    end
  end

  defp find_json_start(input, start_char) do
    case String.split(input, start_char, parts: 2) do
      [before, _after] -> String.length(before)
      [_single] -> nil
    end
  end

  defp extract_balanced_content(input, start_pos, open_char, close_char) do
    substring = String.slice(input, start_pos, String.length(input))

    case find_balanced_end(substring, open_char, close_char) do
      nil -> nil
      end_pos -> String.slice(substring, 0, end_pos + 1)
    end
  end

  defp find_balanced_end(string, open_char, close_char) do
    find_balanced_end(string, open_char, close_char, 0, 0, false)
  end

  defp find_balanced_end(<<>>, _open, _close, _pos, _balance, _in_string), do: nil

  defp find_balanced_end(<<"\"", rest::binary>>, open, close, pos, balance, in_string) do
    find_balanced_end(rest, open, close, pos + 1, balance, not in_string)
  end

  defp find_balanced_end(<<"\\\"", rest::binary>>, open, close, pos, balance, in_string) do
    find_balanced_end(rest, open, close, pos + 2, balance, in_string)
  end

  defp find_balanced_end(<<char::utf8, rest::binary>>, open, close, pos, balance, false)
       when <<char::utf8>> == open do
    find_balanced_end(rest, open, close, pos + 1, balance + 1, false)
  end

  defp find_balanced_end(<<char::utf8, rest::binary>>, open, close, pos, balance, false)
       when <<char::utf8>> == close do
    new_balance = balance - 1

    if new_balance == 0 do
      pos
    else
      find_balanced_end(rest, open, close, pos + 1, new_balance, false)
    end
  end

  defp find_balanced_end(<<_char::utf8, rest::binary>>, open, close, pos, balance, in_string) do
    find_balanced_end(rest, open, close, pos + 1, balance, in_string)
  end

  # Helper functions for string detection using direct methods

  # Fast check for long text that likely contains JSON content
  defp long_text_with_content?(input) do
    byte_size(input) > 100 and
      not (String.starts_with?(input, "{") or String.starts_with?(input, "["))
  end

  defp line_has_comment_in_string?(line) do
    # Simple check: count quotes before //
    case String.split(line, "//", parts: 2) do
      [before_comment, _after] ->
        quote_count = count_unescaped_quotes(before_comment)
        rem(quote_count, 2) != 0

      [_single_part] ->
        false
    end
  end

  defp comment_inside_string?(input, position) do
    string_context_at_position?(input, position)
  end

  defp string_context_at_position?(input, position) do
    before = String.slice(input, 0, position)
    quote_count = count_unescaped_quotes(before)
    rem(quote_count, 2) != 0
  end

  defp count_unescaped_quotes(string) do
    count_unescaped_quotes(string, 0, 0)
  end

  defp count_unescaped_quotes(<<>>, _pos, count), do: count

  defp count_unescaped_quotes(<<"\\\"", rest::binary>>, pos, count) do
    count_unescaped_quotes(rest, pos + 2, count)
  end

  defp count_unescaped_quotes(<<"\"", rest::binary>>, pos, count) do
    count_unescaped_quotes(rest, pos + 1, count + 1)
  end

  defp count_unescaped_quotes(<<_char::utf8, rest::binary>>, pos, count) do
    count_unescaped_quotes(rest, pos + 1, count)
  end

  # Encoding normalization using direct methods
  defp filter_to_ascii(input) do
    input
    |> String.to_charlist()
    |> Enum.filter(fn char -> char >= 0 and char <= 127 end)
    |> List.to_string()
  end
end
</file>

<file path="json_remedy/layer2/structural_repair.ex">
defmodule JsonRemedy.Layer2.StructuralRepair do
  @moduledoc """
  Layer 2: Structural Repair - Fixes missing, extra, and mismatched delimiters using a state machine.

  This layer handles:
  - Missing closing delimiters ({ [
  - Extra closing delimiters } ]
  - Mismatched delimiters (closing with wrong type)
  - Complex nested structure issues

  Uses a state machine approach to track parsing context and handle structural repairs.
  """

  @behaviour JsonRemedy.LayerBehaviour

  alias JsonRemedy.LayerBehaviour

  # Import types from LayerBehaviour
  @type repair_action :: LayerBehaviour.repair_action()
  @type repair_context :: LayerBehaviour.repair_context()
  @type layer_result :: LayerBehaviour.layer_result()

  # State machine types
  @type parser_state :: :root | :object | :array
  @type delimiter_type :: :brace | :bracket
  @type context_frame :: %{type: delimiter_type(), position: non_neg_integer()}

  # More specific state type for the state machine
  @type state_map :: %{
          position: non_neg_integer(),
          current_state: parser_state(),
          context_stack: [context_frame()],
          repairs: [repair_action()],
          in_string: boolean(),
          escape_next: boolean(),
          result_chars: [String.t()],
          input: String.t()
        }

  @doc """
  Process input string and apply Layer 2 structural repairs using state machine.

  Returns:
  - `{:ok, processed_input, updated_context}` - Layer completed successfully
  - `{:continue, input, context}` - Layer doesn't apply, pass to next layer
  - `{:error, reason}` - Layer failed, stop pipeline
  """
  @spec process(input :: String.t(), context :: repair_context()) :: layer_result()
  def process(input, context) do
    # Initialize state machine
    state = %{
      position: 0,
      current_state: :root,
      context_stack: [],
      repairs: [],
      in_string: false,
      escape_next: false,
      result_chars: [],
      # Store input for look-ahead analysis
      input: input
    }

    # Parse character by character
    final_state = parse_string(input, state)

    # Handle any unclosed contexts at end
    final_state = close_unclosed_contexts(final_state)

    # Build result
    result = final_state.result_chars |> Enum.reverse() |> List.to_string()

    updated_context = %{
      repairs: context.repairs ++ final_state.repairs,
      options: context.options,
      metadata: Map.put(Map.get(context, :metadata, %{}), :layer2_processed, true)
    }

    {:ok, result, updated_context}
  rescue
    error ->
      {:error, "Layer 2 Structural Repair failed: #{inspect(error)}"}
  end

  @spec parse_string(input :: String.t(), state :: map()) :: map()
  defp parse_string(input, state) do
    input
    |> String.graphemes()
    |> Enum.with_index()
    |> Enum.reduce(state, fn {char, index}, acc_state ->
      %{acc_state | position: index}
      |> process_character(char)
    end)
  end

  @spec process_character(state :: map(), char :: String.t()) :: map()
  defp process_character(state, char) do
    cond do
      # Handle escape sequences in strings
      state.escape_next ->
        %{state | escape_next: false, result_chars: [char | state.result_chars]}

      # Handle escape character in strings
      state.in_string and char == "\\" ->
        %{state | escape_next: true, result_chars: [char | state.result_chars]}

      # Handle quote characters
      char == "\"" ->
        handle_quote(state, char)

      # Handle structural delimiters outside strings
      not state.in_string ->
        handle_delimiter(state, char)

      # Regular character (including delimiters inside strings)
      true ->
        %{state | result_chars: [char | state.result_chars]}
    end
  end

  @spec handle_quote(state :: state_map(), char :: <<_::8>>) :: state_map()
  defp handle_quote(state, char) do
    %{state | in_string: not state.in_string, result_chars: [char | state.result_chars]}
  end

  @spec handle_delimiter(state :: state_map(), char :: <<_::8>>) :: state_map()
  defp handle_delimiter(state, char) do
    case char do
      "{" ->
        handle_opening_brace(state, char)

      "[" ->
        handle_opening_bracket(state, char)

      "}" ->
        handle_closing_brace(state, char)

      "]" ->
        handle_closing_bracket(state, char)

      _ ->
        # Regular character, just add to result
        %{state | result_chars: [char | state.result_chars]}
    end
  end

  @spec handle_opening_brace(state :: state_map(), char :: <<_::8>>) :: state_map()
  defp handle_opening_brace(state, char) do
    # Check for extra opening braces (like {{ without any content)
    if extra_opening_delimiter?(state, :brace) do
      repair = %{
        layer: :structural_repair,
        action: "removed extra opening brace",
        position: state.position,
        original: char,
        replacement: ""
      }

      # Don't add the extra character to result_chars
      %{state | repairs: [repair | state.repairs]}
    else
      context_frame = %{type: :brace, position: state.position}

      %{
        state
        | context_stack: [context_frame | state.context_stack],
          current_state: :object,
          result_chars: [char | state.result_chars]
      }
    end
  end

  @spec handle_opening_bracket(state :: state_map(), char :: <<_::8>>) :: state_map()
  defp handle_opening_bracket(state, char) do
    # Check for extra opening brackets (like [[ without any content)
    if extra_opening_delimiter?(state, :bracket) do
      repair = %{
        layer: :structural_repair,
        action: "removed extra opening bracket",
        position: state.position,
        original: char,
        replacement: ""
      }

      # Don't add the extra character to result_chars
      %{state | repairs: [repair | state.repairs]}
    else
      context_frame = %{type: :bracket, position: state.position}

      %{
        state
        | context_stack: [context_frame | state.context_stack],
          current_state: :array,
          result_chars: [char | state.result_chars]
      }
    end
  end

  @spec handle_closing_brace(state :: state_map(), char :: <<_::8>>) :: state_map()
  defp handle_closing_brace(state, char) do
    case state.context_stack do
      # No open context - this is an extra closing brace
      [] ->
        repair = %{
          layer: :structural_repair,
          action: "removed extra closing brace",
          position: state.position,
          original: char,
          replacement: ""
        }

        # Don't add the extra character to result_chars
        %{state | repairs: [repair | state.repairs]}

      # Matching brace context
      [%{type: :brace} | rest] ->
        %{
          state
          | context_stack: rest,
            current_state: determine_state_from_stack(rest),
            result_chars: [char | state.result_chars]
        }

      # Mismatched context (bracket opened, brace closed)
      [%{type: :bracket} | rest] ->
        # For mismatches, we need to decide: fix the opener or the closer?
        # Generally, fix the closer to match the opener (array stays array)
        repair = %{
          layer: :structural_repair,
          action: "fixed array-object mismatch: changed } to ]",
          position: state.position,
          original: char,
          replacement: "]"
        }

        %{
          state
          | context_stack: rest,
            current_state: determine_state_from_stack(rest),
            repairs: [repair | state.repairs],
            result_chars: ["]" | state.result_chars]
        }
    end
  end

  @spec handle_closing_bracket(state :: state_map(), char :: <<_::8>>) :: state_map()
  defp handle_closing_bracket(state, char) do
    case state.context_stack do
      # No open context - this is an extra closing bracket
      [] ->
        repair = %{
          layer: :structural_repair,
          action: "removed extra closing bracket",
          position: state.position,
          original: char,
          replacement: ""
        }

        # Don't add the extra character to result_chars
        %{state | repairs: [repair | state.repairs]}

      # Matching bracket context
      [%{type: :bracket} | rest] ->
        %{
          state
          | context_stack: rest,
            current_state: determine_state_from_stack(rest),
            result_chars: [char | state.result_chars]
        }

      # Mismatched context (brace opened, bracket closed)
      [%{type: :brace} | rest] ->
        # For mismatches, fix the closer to match the opener (object stays object)
        repair = %{
          layer: :structural_repair,
          action: "fixed object-array mismatch: changed ] to }",
          position: state.position,
          original: char,
          replacement: "}"
        }

        %{
          state
          | context_stack: rest,
            current_state: determine_state_from_stack(rest),
            repairs: [repair | state.repairs],
            result_chars: ["}" | state.result_chars]
        }
    end
  end

  @spec determine_state_from_stack(stack :: [context_frame()]) :: parser_state()
  defp determine_state_from_stack([]), do: :root
  defp determine_state_from_stack([%{type: :brace} | _]), do: :object
  defp determine_state_from_stack([%{type: :bracket} | _]), do: :array

  @spec extra_opening_delimiter?(state :: state_map(), delimiter_type :: delimiter_type()) ::
          boolean()
  defp extra_opening_delimiter?(state, delimiter_type) do
    case state.context_stack do
      # Check if we have consecutive same delimiters
      [%{type: ^delimiter_type, position: pos} | _] when state.position - pos == 1 ->
        # Look ahead to see if this is likely redundant
        # Redundant patterns:
        # - [[simple_content]] (no comma at top level)
        # - {{simple_content}} (no comma at top level)
        # Valid patterns:
        # - [[item1], [item2]] (comma at top level)
        # - [{item1}, {item2}] (comma at top level)

        remaining_input =
          String.slice(state.input, state.position + 1, String.length(state.input))

        appears_redundant?(remaining_input, delimiter_type)

      _ ->
        false
    end
  end

  @spec appears_redundant?(input :: String.t(), delimiter_type :: delimiter_type()) :: boolean()
  defp appears_redundant?(input, delimiter_type) do
    # Simple heuristic: if there's no comma at the top level of the nested structure,
    # then the outer delimiter is likely redundant
    # This is a simplification but should handle the common cases

    case delimiter_type do
      :bracket ->
        # Look for pattern like [content] without comma at same level
        not String.contains?(input, "], [")

      :brace ->
        # Look for pattern like {content} without comma at same level
        not String.contains?(input, "}, {")
    end
  end

  @spec close_unclosed_contexts(state :: state_map()) :: state_map()
  defp close_unclosed_contexts(%{context_stack: []} = state), do: state

  defp close_unclosed_contexts(state) do
    # Add missing closing delimiters for any unclosed contexts
    # Close in LIFO order (last opened = first closed), so don't reverse
    {closing_chars, repairs} =
      state.context_stack
      |> Enum.reduce({[], []}, fn context_frame, {chars_acc, repairs_acc} ->
        {close_char, repair} =
          case context_frame.type do
            :brace ->
              repair = %{
                layer: :structural_repair,
                action: "added missing closing brace",
                position: state.position + length(chars_acc),
                original: nil,
                replacement: "}"
              }

              {"}", repair}

            :bracket ->
              repair = %{
                layer: :structural_repair,
                action: "added missing closing bracket",
                position: state.position + length(chars_acc),
                original: nil,
                replacement: "]"
              }

              {"]", repair}
          end

        {[close_char | chars_acc], [repair | repairs_acc]}
      end)

    %{
      state
      | context_stack: [],
        current_state: :root,
        repairs: state.repairs ++ repairs,
        result_chars: closing_chars ++ state.result_chars
    }
  end

  # LayerBehaviour callback implementations

  @doc """
  Check if this layer can handle the given input.
  Layer 2 detects structural issues with delimiters.
  """
  @spec supports?(input :: String.t()) :: boolean()
  def supports?(input) when is_binary(input) do
    # Quick heuristic checks for structural issues
    has_structural_issues?(input)
  end

  def supports?(_), do: false

  @spec has_structural_issues?(input :: String.t()) :: boolean()
  defp has_structural_issues?(input) do
    # Count delimiters (basic check)
    {open_braces, close_braces, open_brackets, close_brackets} = count_delimiters(input)

    # Check for obvious imbalances
    open_braces != close_braces or
      open_brackets != close_brackets or
      has_obvious_mismatches?(input)
  end

  @spec count_delimiters(input :: String.t()) ::
          {non_neg_integer(), non_neg_integer(), non_neg_integer(), non_neg_integer()}
  defp count_delimiters(input) do
    input
    |> String.graphemes()
    |> Enum.reduce({0, 0, 0, 0}, fn char, {ob, cb, obr, cbr} ->
      case char do
        "{" -> {ob + 1, cb, obr, cbr}
        "}" -> {ob, cb + 1, obr, cbr}
        "[" -> {ob, cb, obr + 1, cbr}
        "]" -> {ob, cb, obr, cbr + 1}
        _ -> {ob, cb, obr, cbr}
      end
    end)
  end

  @spec has_obvious_mismatches?(input :: String.t()) :: boolean()
  defp has_obvious_mismatches?(input) do
    # Look for patterns like }] or ]{ and duplicated opening/closing
    String.contains?(input, "}]") or
      String.contains?(input, "]{") or
      String.contains?(input, "[}") or
      String.contains?(input, "{]") or
      String.contains?(input, "{{") or
      String.contains?(input, "[[") or
      String.contains?(input, "}}") or
      String.contains?(input, "]]")
  end

  @doc """
  Return the priority order for this layer.
  Layer 2 (Structural Repair) should run after Layer 1 (Content Cleaning).
  """
  @spec priority() :: 2
  def priority, do: 2

  @doc """
  Return a human-readable name for this layer.
  """
  @spec name() :: String.t()
  def name, do: "Structural Repair"

  @doc """
  Validate layer configuration and options.
  Layer 2 accepts options for controlling structural repair behavior.
  """
  @spec validate_options(options :: keyword()) :: :ok | {:error, String.t()}
  def validate_options(options) when is_list(options) do
    valid_keys = [:max_nesting_depth, :timeout_ms, :strict_mode]

    invalid_keys = Keyword.keys(options) -- valid_keys

    if Enum.empty?(invalid_keys) do
      # Validate option values
      case validate_option_values(options) do
        :ok -> :ok
        error -> error
      end
    else
      {:error, "Invalid options: #{inspect(invalid_keys)}. Valid options: #{inspect(valid_keys)}"}
    end
  end

  def validate_options(_), do: {:error, "Options must be a keyword list"}

  defp validate_option_values(options) do
    Enum.reduce_while(options, :ok, fn {key, value}, _acc ->
      case {key, value} do
        {:max_nesting_depth, depth} when is_integer(depth) and depth > 0 ->
          {:cont, :ok}

        {:timeout_ms, timeout} when is_integer(timeout) and timeout > 0 ->
          {:cont, :ok}

        {:strict_mode, mode} when is_boolean(mode) ->
          {:cont, :ok}

        {key, value} when key in [:max_nesting_depth, :timeout_ms] ->
          {:halt, {:error, "Option #{key} must be a positive integer, got: #{inspect(value)}"}}

        {:strict_mode, value} ->
          {:halt, {:error, "Option strict_mode must be a boolean, got: #{inspect(value)}"}}

        {key, value} ->
          {:halt, {:error, "Invalid value for option #{key}: #{inspect(value)}"}}
      end
    end)
  end
end
</file>

<file path="json_remedy/layer3/syntax_normalization.ex">
defmodule JsonRemedy.Layer3.SyntaxNormalization do
  @moduledoc """
  Layer 3: Syntax Normalization - Fixes JSON syntax issues using character-by-character parsing.

  This layer handles:
  - Quote normalization (single → double quotes)
  - Unquoted keys (add missing quotes)
  - Boolean/null normalization (True/False/None → true/false/null)
  - Comma and colon fixes (trailing commas, missing commas/colons)

  Uses character-by-character parsing to be context-aware and preserve string content.
  """

  @behaviour JsonRemedy.LayerBehaviour

  alias JsonRemedy.LayerBehaviour

  # Import types from LayerBehaviour
  @type repair_action :: LayerBehaviour.repair_action()
  @type repair_context :: LayerBehaviour.repair_context()
  @type layer_result :: LayerBehaviour.layer_result()

  # Layer-specific types as per API contract
  @type syntax_rule :: %{
          name: String.t(),
          processor: (String.t() -> {String.t(), [repair_action()]}),
          condition: (String.t() -> boolean()) | nil
        }

  @type parse_state :: %{
          result: String.t(),
          position: non_neg_integer(),
          in_string: boolean(),
          escape_next: boolean(),
          string_quote: String.t() | nil,
          repairs: [repair_action()],
          context_stack: [:object | :array],
          expecting: :key | :value | :colon | :comma_or_end
        }

  @doc """
  Check if this layer can handle the given input.
  """
  @spec supports?(input :: String.t()) :: boolean()
  def supports?(input) when is_binary(input) do
    has_syntax_issues?(input)
  end

  def supports?(_), do: false

  @doc """
  Process input string and apply Layer 3 syntax normalization repairs.
  """
  @spec process(input :: String.t(), context :: repair_context()) :: layer_result()
  def process(input, context) when is_binary(input) and is_map(context) do
    try do
      {fixed_content, repairs} = normalize_syntax(input)

      updated_context = %{
        repairs: context.repairs ++ repairs,
        options: context.options,
        metadata: Map.put(Map.get(context, :metadata, %{}), :layer3_applied, true)
      }

      {:ok, fixed_content, updated_context}
    rescue
      error ->
        {:error, "Layer 3 Syntax Normalization failed: #{inspect(error)}"}
    end
  end

  # Handle nil or invalid inputs gracefully
  def process(nil, context) when is_map(context) do
    {:error, "Input cannot be nil"}
  end

  def process(input, _context) when not is_binary(input) do
    {:error, "Input must be a string, got: #{inspect(input)}"}
  end

  def process(_input, context) when not is_map(context) do
    {:error, "Context must be a map, got: #{inspect(context)}"}
  end

  @doc """
  Return the priority order for this layer.
  Layer 3 (Syntax Normalization) should run after structural repair.
  """
  @spec priority() :: 3
  def priority, do: 3

  @doc """
  Return a human-readable name for this layer.
  """
  @spec name() :: String.t()
  def name, do: "Syntax Normalization"

  @doc """
  Validate layer configuration and options.
  """
  @spec validate_options(options :: keyword()) :: :ok | {:error, String.t()}
  def validate_options(options) when is_list(options) do
    # Check for unknown options
    known_options = [
      :strict_mode,
      :preserve_formatting,
      :normalize_quotes,
      :normalize_booleans,
      :fix_commas
    ]

    unknown_options = Keyword.keys(options) -- known_options

    if unknown_options != [] do
      {:error, "Invalid options: #{inspect(unknown_options)}"}
    else
      # Check option values
      case validate_option_values(options) do
        :ok -> :ok
        error -> error
      end
    end
  end

  def validate_options(_), do: {:error, "Options must be a keyword list"}

  # Validate option values
  defp validate_option_values(options) do
    Enum.reduce_while(options, :ok, fn {key, value}, _acc ->
      case validate_option_value(key, value) do
        :ok -> {:cont, :ok}
        error -> {:halt, error}
      end
    end)
  end

  defp validate_option_value(key, value)
       when key in [
              :normalize_quotes,
              :normalize_booleans,
              :fix_commas,
              :strict_mode,
              :preserve_formatting
            ] do
    if is_boolean(value) do
      :ok
    else
      {:error, "Option #{key} must be a boolean"}
    end
  end

  defp validate_option_value(_key, _value), do: :ok

  @doc """
  Public API function to normalize quotes only.
  """
  @spec normalize_quotes(input :: String.t()) :: {String.t(), [repair_action()]}
  def normalize_quotes(input) when is_binary(input) do
    # Run only quote normalization
    initial_state = %{
      result: "",
      position: 0,
      in_string: false,
      escape_next: false,
      string_quote: nil,
      repairs: [],
      context_stack: [],
      expecting: :value
    }

    final_state = parse_characters_quotes_only(input, initial_state)
    {final_state.result, final_state.repairs}
  end

  # Handle nil input gracefully
  def normalize_quotes(nil), do: {"", []}
  def normalize_quotes(input) when not is_binary(input), do: {inspect(input), []}

  @doc """
  Public API function to normalize booleans only.
  """
  @spec normalize_booleans(input :: String.t()) :: {String.t(), [repair_action()]}
  def normalize_booleans(input) when is_binary(input) do
    # Simple replacement for boolean normalization
    result =
      input
      |> String.replace("True", "true")
      |> String.replace("False", "false")
      |> String.replace("TRUE", "true")
      |> String.replace("FALSE", "false")

    repairs = []

    repairs =
      if String.contains?(input, "True") do
        [create_repair("normalized boolean", "Normalized True -> true", 0) | repairs]
      else
        repairs
      end

    repairs =
      if String.contains?(input, "False") do
        [create_repair("normalized boolean", "Normalized False -> false", 0) | repairs]
      else
        repairs
      end

    repairs =
      if String.contains?(input, "TRUE") do
        [create_repair("normalized boolean", "Normalized TRUE -> true", 0) | repairs]
      else
        repairs
      end

    repairs =
      if String.contains?(input, "FALSE") do
        [create_repair("normalized boolean", "Normalized FALSE -> false", 0) | repairs]
      else
        repairs
      end

    {result, repairs}
  end

  # Handle nil input gracefully
  def normalize_booleans(nil), do: {"", []}
  def normalize_booleans(input) when not is_binary(input), do: {inspect(input), []}

  @doc """
  Public API function to fix commas only.
  """
  @spec fix_commas(input :: String.t()) :: {String.t(), [repair_action()]}
  def fix_commas(input) when is_binary(input) do
    post_process_commas(input)
  end

  # Handle nil input gracefully
  def fix_commas(nil), do: {"", []}
  def fix_commas(input) when not is_binary(input), do: {inspect(input), []}

  @doc """
  Get default syntax normalization rules.
  """
  @spec default_rules() :: [syntax_rule()]
  def default_rules do
    [
      %{
        name: "quote_unquoted_keys",
        processor: &quote_unquoted_keys_processor/1,
        condition: nil
      },
      %{
        name: "normalize_single_quotes",
        processor: &normalize_quotes_processor/1,
        condition: nil
      },
      %{
        name: "normalize_booleans_and_nulls",
        processor: &normalize_literals_processor/1,
        condition: nil
      },
      %{
        name: "fix_trailing_commas",
        processor: &fix_trailing_commas_processor/1,
        condition: nil
      }
    ]
  end

  # Processor functions for rules (non-regex implementations)
  defp quote_unquoted_keys_processor(input) when is_binary(input) do
    quote_unquoted_keys_direct(input)
  end

  defp quote_unquoted_keys_processor(nil), do: {"", []}
  defp quote_unquoted_keys_processor(input), do: {inspect(input), []}

  defp normalize_quotes_processor(input) when is_binary(input) do
    normalize_quotes(input)
  end

  defp normalize_quotes_processor(nil), do: {"", []}
  defp normalize_quotes_processor(input), do: {inspect(input), []}

  defp normalize_literals_processor(input) when is_binary(input) do
    normalize_literals_direct(input)
  end

  defp normalize_literals_processor(nil), do: {"", []}
  defp normalize_literals_processor(input), do: {inspect(input), []}

  # Optimized single-pass implementation of normalize_literals
  defp normalize_literals_direct(input) when is_binary(input) do
    # Define all literal replacements
    replacements = [
      {"True", "true", "normalized boolean True -> true"},
      {"False", "false", "normalized boolean False -> false"},
      {"TRUE", "true", "normalized boolean TRUE -> true"},
      {"FALSE", "false", "normalized boolean FALSE -> false"},
      {"None", "null", "normalized None -> null"},
      {"NULL", "null", "normalized NULL -> null"},
      {"Null", "null", "normalized Null -> null"}
    ]

    replace_all_literals_single_pass(input, "", 0, false, false, nil, [], replacements)
  end

  # Single-pass replacement for all literals - UTF-8 safe
  defp replace_all_literals_single_pass(
         input,
         result,
         pos,
         in_string,
         escape_next,
         quote_char,
         repairs,
         replacements
       )
       when is_binary(input) do
    # UTF-8 safe bounds checking using String.length
    if pos >= String.length(input) do
      {result, repairs}
    else
      replace_all_literals_single_pass_continue(
        input,
        result,
        pos,
        in_string,
        escape_next,
        quote_char,
        repairs,
        replacements
      )
    end
  end

  defp replace_all_literals_single_pass_continue(
         input,
         result,
         pos,
         in_string,
         escape_next,
         quote_char,
         repairs,
         replacements
       ) do
    char = String.at(input, pos)

    cond do
      escape_next ->
        replace_all_literals_single_pass(
          input,
          result <> char,
          pos + 1,
          in_string,
          false,
          quote_char,
          repairs,
          replacements
        )

      in_string && char == "\\" ->
        replace_all_literals_single_pass(
          input,
          result <> char,
          pos + 1,
          in_string,
          true,
          quote_char,
          repairs,
          replacements
        )

      in_string && char == quote_char ->
        replace_all_literals_single_pass(
          input,
          result <> char,
          pos + 1,
          false,
          false,
          nil,
          repairs,
          replacements
        )

      in_string ->
        replace_all_literals_single_pass(
          input,
          result <> char,
          pos + 1,
          in_string,
          false,
          quote_char,
          repairs,
          replacements
        )

      char == "\"" || char == "'" ->
        replace_all_literals_single_pass(
          input,
          result <> char,
          pos + 1,
          true,
          false,
          char,
          repairs,
          replacements
        )

      !in_string ->
        # Check all possible literal replacements
        case find_matching_literal(input, pos, replacements) do
          {:match, search_token, replacement_token, repair_description} ->
            if is_word_boundary(input, pos, search_token) do
              repair = create_repair("normalized literal", repair_description, pos)

              replace_all_literals_single_pass(
                input,
                result <> replacement_token,
                pos + String.length(search_token),
                false,
                false,
                nil,
                [repair | repairs],
                replacements
              )
            else
              replace_all_literals_single_pass(
                input,
                result <> char,
                pos + 1,
                in_string,
                false,
                quote_char,
                repairs,
                replacements
              )
            end

          :no_match ->
            replace_all_literals_single_pass(
              input,
              result <> char,
              pos + 1,
              in_string,
              false,
              quote_char,
              repairs,
              replacements
            )
        end

      true ->
        replace_all_literals_single_pass(
          input,
          result <> char,
          pos + 1,
          in_string,
          false,
          quote_char,
          repairs,
          replacements
        )
    end
  end

  # Find matching literal at current position
  defp find_matching_literal(input, pos, replacements) do
    find_matching_literal_recursive(input, pos, replacements)
  end

  defp find_matching_literal_recursive(_input, _pos, []) do
    :no_match
  end

  defp find_matching_literal_recursive(input, pos, [{search, replacement, description} | rest]) do
    if match_at_position?(input, pos, search) do
      {:match, search, replacement, description}
    else
      find_matching_literal_recursive(input, pos, rest)
    end
  end

  # Check if a string matches at a specific position (UTF-8 safe)
  defp match_at_position?(input, pos, search_string) do
    search_length = String.length(search_string)

    if pos + search_length > String.length(input) do
      false
    else
      substring = String.slice(input, pos, search_length)
      substring == search_string
    end
  end

  # Check if a token match is at a word boundary
  defp is_word_boundary(input, pos, token) do
    token_length = String.length(token)

    # Check character before token
    before_ok =
      if pos == 0 do
        true
      else
        prev_char = String.at(input, pos - 1)
        !is_identifier_char(prev_char)
      end

    # Check character after token
    after_ok =
      if pos + token_length >= String.length(input) do
        true
      else
        next_char = String.at(input, pos + token_length)
        !is_identifier_char(next_char)
      end

    before_ok && after_ok
  end

  # Consume whitespace and return {whitespace_string, new_position}
  defp consume_whitespace(input, pos) when is_binary(input) and is_integer(pos) do
    consume_whitespace_acc(input, pos, pos, "")
  end

  # UTF-8 safe version using String.length
  defp consume_whitespace_acc(input, current_pos, _start_pos, acc) do
    if current_pos >= String.length(input) do
      {acc, current_pos}
    else
      consume_whitespace_acc_continue(input, current_pos, acc)
    end
  end

  defp consume_whitespace_acc_continue(input, current_pos, acc) do
    char = String.at(input, current_pos)

    if char in [" ", "\t", "\n", "\r"] do
      consume_whitespace_acc(input, current_pos + 1, nil, acc <> char)
    else
      {acc, current_pos}
    end
  end

  defp fix_trailing_commas_processor(input) when is_binary(input) do
    fix_commas(input)
  end

  @doc """
  Check if a position in the input is inside a string literal.
  Used to avoid applying repairs to string content.
  """
  @spec inside_string?(input :: String.t(), position :: non_neg_integer()) :: boolean()
  def inside_string?(input, position)
      when is_binary(input) and is_integer(position) and position >= 0 do
    check_string_context(input, position, 0, false, false, nil)
  end

  # Handle invalid inputs gracefully
  def inside_string?(nil, _position), do: false
  def inside_string?(_input, position) when not is_integer(position), do: false
  def inside_string?(_input, position) when position < 0, do: false
  def inside_string?(input, _position) when not is_binary(input), do: false

  # Helper function to check if position is inside a string
  defp check_string_context(_input, position, current_pos, in_string, _escape_next, _quote)
       when current_pos >= position do
    in_string
  end

  defp check_string_context(input, position, current_pos, in_string, escape_next, quote) do
    if current_pos >= String.length(input) do
      in_string
    else
      char = String.at(input, current_pos)

      cond do
        escape_next ->
          check_string_context(input, position, current_pos + 1, in_string, false, quote)

        in_string && char == "\\" ->
          check_string_context(input, position, current_pos + 1, in_string, true, quote)

        in_string && char == quote ->
          check_string_context(input, position, current_pos + 1, false, false, nil)

        in_string ->
          check_string_context(input, position, current_pos + 1, in_string, false, quote)

        char == "\"" ->
          check_string_context(input, position, current_pos + 1, true, false, "\"")

        char == "'" ->
          check_string_context(input, position, current_pos + 1, true, false, "'")

        true ->
          check_string_context(input, position, current_pos + 1, false, false, nil)
      end
    end
  end

  @doc """
  Apply a single syntax rule with context awareness.
  """
  @spec apply_rule(input :: String.t(), rule :: syntax_rule()) ::
          {String.t(), [repair_action()]}
  def apply_rule(input, rule) do
    if rule.condition && !rule.condition.(input) do
      {input, []}
    else
      rule.processor.(input)
    end
  end

  @doc """
  Add quotes around unquoted keys.
  """
  @spec quote_unquoted_keys(input :: String.t()) :: {String.t(), [repair_action()]}
  def quote_unquoted_keys(input) when is_binary(input) do
    quote_unquoted_keys_direct(input)
  end

  def quote_unquoted_keys(nil), do: {"", []}
  def quote_unquoted_keys(input) when not is_binary(input), do: {inspect(input), []}

  # Direct implementation of quote_unquoted_keys without regex
  defp quote_unquoted_keys_direct(input) do
    quote_unquoted_keys_char_by_char(input, "", 0, false, false, nil, [])
  end

  # UTF-8 safe version
  defp quote_unquoted_keys_char_by_char(
         input,
         result,
         pos,
         in_string,
         escape_next,
         quote_char,
         repairs
       ) do
    if pos >= String.length(input) do
      {result, repairs}
    else
      quote_unquoted_keys_char_by_char_continue(
        input,
        result,
        pos,
        in_string,
        escape_next,
        quote_char,
        repairs
      )
    end
  end

  defp quote_unquoted_keys_char_by_char_continue(
         input,
         result,
         pos,
         in_string,
         escape_next,
         quote_char,
         repairs
       ) do
    char = String.at(input, pos)

    cond do
      escape_next ->
        quote_unquoted_keys_char_by_char(
          input,
          result <> char,
          pos + 1,
          in_string,
          false,
          quote_char,
          repairs
        )

      in_string && char == "\\" ->
        quote_unquoted_keys_char_by_char(
          input,
          result <> char,
          pos + 1,
          in_string,
          true,
          quote_char,
          repairs
        )

      in_string && char == quote_char ->
        quote_unquoted_keys_char_by_char(
          input,
          result <> char,
          pos + 1,
          false,
          false,
          nil,
          repairs
        )

      in_string ->
        quote_unquoted_keys_char_by_char(
          input,
          result <> char,
          pos + 1,
          in_string,
          false,
          quote_char,
          repairs
        )

      char == "\"" || char == "'" ->
        quote_unquoted_keys_char_by_char(
          input,
          result <> char,
          pos + 1,
          true,
          false,
          char,
          repairs
        )

      !in_string && (char == "{" || char == ",") ->
        # Look ahead for unquoted key after { or ,
        {new_result, new_pos, new_repairs} =
          maybe_quote_next_key(input, result <> char, pos + 1, repairs)

        quote_unquoted_keys_char_by_char(
          input,
          new_result,
          new_pos,
          false,
          false,
          nil,
          new_repairs
        )

      true ->
        quote_unquoted_keys_char_by_char(
          input,
          result <> char,
          pos + 1,
          in_string,
          false,
          quote_char,
          repairs
        )
    end
  end

  # UTF-8 safe version
  defp maybe_quote_next_key(input, result, pos, repairs) do
    if pos >= String.length(input) do
      {result, pos, repairs}
    else
      maybe_quote_next_key_process(input, result, pos, repairs)
    end
  end

  defp maybe_quote_next_key_process(input, result, pos, repairs) do
    # Skip whitespace
    {whitespace, new_pos} = consume_whitespace(input, pos)

    if new_pos >= String.length(input) do
      {result <> whitespace, new_pos, repairs}
    else
      char = String.at(input, new_pos)

      if is_identifier_start(char) do
        # Found potential unquoted key
        {identifier, chars_consumed} = consume_identifier(input, new_pos)
        after_identifier_pos = new_pos + chars_consumed

        # Check if followed by colon (possibly with whitespace)
        {whitespace_after, pos_after_ws} = consume_whitespace(input, after_identifier_pos)

        if pos_after_ws < String.length(input) && String.at(input, pos_after_ws) == ":" do
          # This is an unquoted key - add quotes
          repair =
            create_repair(
              "quoted unquoted key",
              "Added quotes around unquoted key '#{identifier}'",
              new_pos
            )

          new_result = result <> whitespace <> "\"" <> identifier <> "\"" <> whitespace_after
          {new_result, pos_after_ws, [repair | repairs]}
        else
          # Not a key, just regular content
          {result <> whitespace <> identifier, after_identifier_pos, repairs}
        end
      else
        # Not an identifier start
        {result <> whitespace, new_pos, repairs}
      end
    end
  end

  @doc """
  Normalize boolean and null literals.
  """
  @spec normalize_literals(input :: String.t()) :: {String.t(), [repair_action()]}
  def normalize_literals(input) when is_binary(input) do
    normalize_literals_direct(input)
  end

  def normalize_literals(nil), do: {"", []}
  def normalize_literals(input) when not is_binary(input), do: {inspect(input), []}

  @doc """
  Add missing colons in object key-value pairs.
  """
  @spec fix_colons(input :: String.t()) :: {String.t(), [repair_action()]}
  def fix_colons(input) when is_binary(input) do
    add_missing_colons(input, [])
  end

  def fix_colons(nil), do: {"", []}
  def fix_colons(input) when not is_binary(input), do: {inspect(input), []}

  @doc """
  Validate that a syntax rule is well-formed.
  """
  @spec validate_rule(rule :: syntax_rule()) :: :ok | {:error, String.t()}
  def validate_rule(rule) do
    cond do
      !is_binary(rule.name) ->
        {:error, "Rule name must be a string"}

      !is_function(rule.processor, 1) ->
        {:error, "Rule processor must be a function/1"}

      rule.condition && !is_function(rule.condition, 1) ->
        {:error, "Rule condition must be a function/1 or nil"}

      true ->
        :ok
    end
  end

  @doc """
  Get position information for error reporting.
  """
  @spec get_position_info(input :: String.t(), position :: non_neg_integer()) ::
          %{line: pos_integer(), column: pos_integer(), context: String.t()}
  def get_position_info(input, position)
      when is_binary(input) and is_integer(position) and position >= 0 do
    lines = String.split(input, "\n")

    {line_num, column, _} =
      Enum.reduce_while(lines, {1, 1, 0}, fn line, {current_line, _col, char_count} ->
        # +1 for newline
        line_length = String.length(line) + 1

        if char_count + line_length > position do
          column = position - char_count + 1
          {:halt, {current_line, column, char_count}}
        else
          {:cont, {current_line + 1, 1, char_count + line_length}}
        end
      end)

    context_start = max(0, position - 20)
    context_end = min(String.length(input), position + 20)
    context = String.slice(input, context_start, context_end - context_start)

    %{
      line: line_num,
      column: column,
      context: context
    }
  end

  # Handle invalid inputs gracefully
  def get_position_info(nil, _position) do
    %{line: 1, column: 1, context: ""}
  end

  def get_position_info(_input, position) when not is_integer(position) or position < 0 do
    %{line: 1, column: 1, context: ""}
  end

  def get_position_info(input, _position) when not is_binary(input) do
    %{line: 1, column: 1, context: inspect(input)}
  end

  # Main normalization function using character-by-character parsing
  defp normalize_syntax(content) do
    initial_state = %{
      result: "",
      position: 0,
      in_string: false,
      escape_next: false,
      string_quote: nil,
      repairs: [],
      context_stack: [],
      expecting: :value
    }

    final_state = parse_characters(content, initial_state)

    # Post-process to remove trailing commas and add missing commas
    {comma_processed, comma_repairs} = post_process_commas(final_state.result)

    # Post-process to add missing colons
    {colon_processed, colon_repairs} = add_missing_colons(comma_processed, [])

    all_repairs = final_state.repairs ++ comma_repairs ++ colon_repairs
    {colon_processed, all_repairs}
  end

  # Character-by-character parser - UTF-8 safe
  defp parse_characters(content, state) do
    if state.position >= String.length(content) do
      state
    else
      char = String.at(content, state.position)
      new_state = process_character(char, content, state)
      parse_characters(content, %{new_state | position: new_state.position + 1})
    end
  end

  # Character-by-character parser for quotes only - UTF-8 safe
  defp parse_characters_quotes_only(content, state) do
    if state.position >= String.length(content) do
      state
    else
      char = String.at(content, state.position)
      new_state = process_character_quotes_only(char, content, state)
      parse_characters_quotes_only(content, %{new_state | position: new_state.position + 1})
    end
  end

  # Process characters for quote normalization only
  defp process_character_quotes_only(char, _content, state) do
    cond do
      state.escape_next ->
        # Previous character was escape, add this character as-is
        %{state | result: state.result <> char, escape_next: false}

      state.in_string && char == "\\" ->
        # Escape character in string
        %{state | result: state.result <> char, escape_next: true}

      state.in_string && char == state.string_quote ->
        # End of string
        %{
          state
          | # Always use double quotes
            result: state.result <> "\"",
            in_string: false,
            string_quote: nil
        }

      state.in_string ->
        # Regular character inside string - preserve as-is
        %{state | result: state.result <> char}

      char == "\"" ->
        # Start of double-quoted string
        %{state | result: state.result <> "\"", in_string: true, string_quote: "\""}

      char == "'" ->
        # Start of single-quoted string - normalize to double quotes
        repair =
          create_repair(
            "normalized quotes",
            "Changed single quotes to double quotes",
            state.position
          )

        %{
          state
          | result: state.result <> "\"",
            in_string: true,
            string_quote: "'",
            repairs: [repair | state.repairs]
        }

      true ->
        # Other character - pass through
        %{state | result: state.result <> char}
    end
  end

  # Process individual characters with context awareness
  defp process_character(char, content, state) do
    cond do
      state.escape_next ->
        # Previous character was escape, add this character as-is
        %{state | result: state.result <> char, escape_next: false}

      state.in_string && char == "\\" ->
        # Escape character in string
        %{state | result: state.result <> char, escape_next: true}

      state.in_string && char == state.string_quote ->
        # End of string
        %{
          state
          | # Always use double quotes
            result: state.result <> "\"",
            in_string: false,
            string_quote: nil,
            expecting: determine_next_expecting(state)
        }

      state.in_string ->
        # Regular character inside string - preserve as-is
        %{state | result: state.result <> char}

      char == "\"" ->
        # Start of double-quoted string
        %{
          state
          | result: state.result <> "\"",
            in_string: true,
            string_quote: "\"",
            expecting: determine_next_expecting(state)
        }

      char == "'" ->
        # Start of single-quoted string - normalize to double quotes
        repair =
          create_repair(
            "normalized quotes",
            "Changed single quotes to double quotes",
            state.position
          )

        %{
          state
          | result: state.result <> "\"",
            in_string: true,
            string_quote: "'",
            repairs: [repair | state.repairs],
            expecting: determine_next_expecting(state)
        }

      char == "{" ->
        # Object start
        %{
          state
          | result: state.result <> char,
            context_stack: [:object | state.context_stack],
            expecting: :key
        }

      char == "}" ->
        # Object end
        {new_stack, _} = pop_stack_safe(state.context_stack)

        %{
          state
          | result: state.result <> char,
            context_stack: new_stack,
            expecting: determine_expecting_after_close(new_stack)
        }

      char == "[" ->
        # Array start
        %{
          state
          | result: state.result <> char,
            context_stack: [:array | state.context_stack],
            expecting: :value
        }

      char == "]" ->
        # Array end
        {new_stack, _} = pop_stack_safe(state.context_stack)

        %{
          state
          | result: state.result <> char,
            context_stack: new_stack,
            expecting: determine_expecting_after_close(new_stack)
        }

      char == ":" ->
        # Colon
        %{state | result: state.result <> char, expecting: :value}

      char == "," ->
        # Comma
        new_expecting =
          case List.first(state.context_stack) do
            :object -> :key
            :array -> :value
            _ -> :value
          end

        %{state | result: state.result <> char, expecting: new_expecting}

      char in [" ", "\t", "\n", "\r"] ->
        # Whitespace - preserve but don't change expectations
        %{state | result: state.result <> char}

      is_identifier_start(char) ->
        # Start of identifier - could be unquoted key, boolean, null, etc.
        process_identifier(content, state)

      char in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-", "+"] ->
        # Start of number
        process_number(content, state)

      true ->
        # Other character - pass through
        %{state | result: state.result <> char}
    end
  end

  # Process identifiers (unquoted keys, booleans, null values)
  defp process_identifier(content, state) do
    {identifier, chars_consumed} = consume_identifier(content, state.position)

    cond do
      # Check for boolean values that need normalization
      identifier in ["True", "TRUE"] ->
        repair =
          create_repair(
            "normalized boolean",
            "Normalized boolean #{identifier} -> true",
            state.position
          )

        %{
          state
          | result: state.result <> "true",
            position: state.position + chars_consumed - 1,
            repairs: [repair | state.repairs],
            expecting: determine_next_expecting(state)
        }

      identifier in ["False", "FALSE"] ->
        repair =
          create_repair(
            "normalized boolean",
            "Normalized boolean #{identifier} -> false",
            state.position
          )

        %{
          state
          | result: state.result <> "false",
            position: state.position + chars_consumed - 1,
            repairs: [repair | state.repairs],
            expecting: determine_next_expecting(state)
        }

      # Check for null values that need normalization
      identifier in ["None", "NULL", "Null"] ->
        repair =
          create_repair("normalized null", "Normalized #{identifier} -> null", state.position)

        %{
          state
          | result: state.result <> "null",
            position: state.position + chars_consumed - 1,
            repairs: [repair | state.repairs],
            expecting: determine_next_expecting(state)
        }

      # Check if this should be a quoted key
      state.expecting == :key ->
        repair =
          create_repair(
            "quoted unquoted key",
            "Added quotes around unquoted key '#{identifier}'",
            state.position
          )

        %{
          state
          | result: state.result <> "\"" <> identifier <> "\"",
            position: state.position + chars_consumed - 1,
            repairs: [repair | state.repairs],
            expecting: :colon
        }

      # Standard literals that don't need normalization
      identifier in ["true", "false", "null"] ->
        %{
          state
          | result: state.result <> identifier,
            position: state.position + chars_consumed - 1,
            expecting: determine_next_expecting(state)
        }

      true ->
        # Unknown identifier - pass through
        %{
          state
          | result: state.result <> identifier,
            position: state.position + chars_consumed - 1
        }
    end
  end

  # Process numbers
  defp process_number(content, state) do
    {number, chars_consumed} = consume_number(content, state.position)

    %{
      state
      | result: state.result <> number,
        position: state.position + chars_consumed - 1,
        expecting: determine_next_expecting(state)
    }
  end

  # Consume identifier characters
  defp consume_identifier(content, start_pos) do
    consume_while(content, start_pos, &is_identifier_char/1)
  end

  # Consume number characters
  defp consume_number(content, start_pos) do
    consume_while(content, start_pos, &is_number_char/1)
  end

  # Generic consume while predicate is true
  defp consume_while(content, start_pos, predicate) do
    consume_while_acc(content, start_pos, start_pos, predicate, "")
  end

  defp consume_while_acc(content, current_pos, start_pos, predicate, acc) do
    # Add bounds checking for UTF-8 safety
    if current_pos >= String.length(content) do
      {acc, current_pos - start_pos}
    else
      char = String.at(content, current_pos)

      if char && predicate.(char) do
        consume_while_acc(content, current_pos + 1, start_pos, predicate, acc <> char)
      else
        {acc, current_pos - start_pos}
      end
    end
  end

  # Post-process to handle comma issues
  defp post_process_commas(content) when is_binary(content) do
    {without_trailing, trailing_repairs} = remove_trailing_commas(content, [])
    {with_missing, missing_repairs} = add_missing_commas(without_trailing, [])
    {with_missing, trailing_repairs ++ missing_repairs}
  end

  # Remove trailing commas
  defp remove_trailing_commas(content, repairs) when is_binary(content) do
    remove_trailing_commas_recursive(content, "", false, false, nil, 0, repairs)
  end

  defp remove_trailing_commas_recursive("", acc, _in_string, _escape_next, _quote, _pos, repairs) do
    {acc, repairs}
  end

  defp remove_trailing_commas_recursive(content, acc, in_string, escape_next, quote, pos, repairs) do
    <<char::utf8, rest::binary>> = content
    char_str = <<char::utf8>>

    cond do
      escape_next ->
        remove_trailing_commas_recursive(
          rest,
          acc <> char_str,
          in_string,
          false,
          quote,
          pos + 1,
          repairs
        )

      in_string && char_str == "\\" ->
        remove_trailing_commas_recursive(
          rest,
          acc <> char_str,
          in_string,
          true,
          quote,
          pos + 1,
          repairs
        )

      in_string && char_str == quote ->
        remove_trailing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs
        )

      in_string ->
        remove_trailing_commas_recursive(
          rest,
          acc <> char_str,
          in_string,
          escape_next,
          quote,
          pos + 1,
          repairs
        )

      char_str == "\"" ->
        remove_trailing_commas_recursive(
          rest,
          acc <> char_str,
          true,
          false,
          "\"",
          pos + 1,
          repairs
        )

      char_str == "," ->
        # Check if this is a trailing comma
        if is_trailing_comma?(rest) do
          repair = create_repair("removed trailing comma", "Removed trailing comma", pos)

          remove_trailing_commas_recursive(rest, acc, false, false, nil, pos + 1, [
            repair | repairs
          ])
        else
          remove_trailing_commas_recursive(
            rest,
            acc <> char_str,
            false,
            false,
            nil,
            pos + 1,
            repairs
          )
        end

      true ->
        remove_trailing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs
        )
    end
  end

  # Check if a comma is trailing (followed only by whitespace and closing delimiter)
  defp is_trailing_comma?(remaining) do
    trimmed = String.trim_leading(remaining)
    String.starts_with?(trimmed, "}") || String.starts_with?(trimmed, "]")
  end

  # Add missing commas (simplified implementation)
  defp add_missing_commas(content, repairs) when is_binary(content) do
    {result, new_repairs} =
      add_missing_commas_recursive(content, "", false, false, nil, 0, repairs, nil, false)

    {result, new_repairs}
  end

  defp add_missing_commas_recursive(
         "",
         acc,
         _in_string,
         _escape_next,
         _quote,
         _pos,
         repairs,
         _prev_token,
         _in_object
       ) do
    {acc, repairs}
  end

  defp add_missing_commas_recursive(
         content,
         acc,
         in_string,
         escape_next,
         quote,
         pos,
         repairs,
         prev_token,
         in_object
       ) do
    <<char::utf8, rest::binary>> = content
    char_str = <<char::utf8>>

    cond do
      escape_next ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          in_string,
          false,
          quote,
          pos + 1,
          repairs,
          prev_token,
          in_object
        )

      in_string && char_str == "\\" ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          in_string,
          true,
          quote,
          pos + 1,
          repairs,
          prev_token,
          in_object
        )

      in_string && char_str == quote ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs,
          :string,
          in_object
        )

      in_string ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          in_string,
          escape_next,
          quote,
          pos + 1,
          repairs,
          prev_token,
          in_object
        )

      char_str == "\"" ->
        # Check if we need a comma before this string
        {new_acc, new_repairs} =
          maybe_add_comma_before_string(acc, pos, repairs, prev_token, in_object, rest)

        add_missing_commas_recursive(
          rest,
          new_acc <> char_str,
          true,
          false,
          "\"",
          pos + 1,
          new_repairs,
          nil,
          in_object
        )

      char_str == "{" ->
        {new_acc, new_repairs} =
          maybe_add_comma_before_value(acc, pos, repairs, prev_token, in_object)

        add_missing_commas_recursive(
          rest,
          new_acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          new_repairs,
          nil,
          true
        )

      char_str == "}" ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs,
          :object_end,
          false
        )

      char_str == "[" ->
        {new_acc, new_repairs} =
          maybe_add_comma_before_value(acc, pos, repairs, prev_token, in_object)

        add_missing_commas_recursive(
          rest,
          new_acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          new_repairs,
          nil,
          false
        )

      char_str == "]" ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs,
          :array_end,
          in_object
        )

      char_str in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] ->
        # Start of number - check if we need comma
        {new_acc, new_repairs} =
          maybe_add_comma_before_value(acc, pos, repairs, prev_token, in_object)

        {number, chars_consumed} = consume_number_in_add_commas(content, 0)

        add_missing_commas_recursive(
          String.slice(content, chars_consumed, String.length(content)),
          new_acc <> number,
          false,
          false,
          nil,
          pos + chars_consumed,
          new_repairs,
          :number,
          in_object
        )

      char_str in [" ", "\t", "\n", "\r"] ->
        # Whitespace - pass through without changing token state
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs,
          prev_token,
          in_object
        )

      char_str == ":" ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs,
          :colon,
          in_object
        )

      char_str == "," ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs,
          :comma,
          in_object
        )

      true ->
        add_missing_commas_recursive(
          rest,
          acc <> char_str,
          false,
          false,
          nil,
          pos + 1,
          repairs,
          :other,
          in_object
        )
    end
  end

  defp maybe_add_comma_before_value(acc, pos, repairs, prev_token, in_object) do
    # Check if we need to add a comma before this value
    case prev_token do
      :string when not in_object ->
        # In array: string followed by string needs comma
        repair =
          create_repair("added missing comma", "Added missing comma between array values", pos)

        {String.trim_trailing(acc) <> ", ", [repair | repairs]}

      :number when not in_object ->
        # In array: number followed by value needs comma
        repair =
          create_repair("added missing comma", "Added missing comma between array values", pos)

        {String.trim_trailing(acc) <> ", ", [repair | repairs]}

      :array_end when not in_object ->
        # In array: array followed by value needs comma
        repair =
          create_repair("added missing comma", "Added missing comma between array values", pos)

        {String.trim_trailing(acc) <> ", ", [repair | repairs]}

      :object_end when not in_object ->
        # In array: object followed by value needs comma
        repair =
          create_repair("added missing comma", "Added missing comma between array values", pos)

        {String.trim_trailing(acc) <> ", ", [repair | repairs]}

      _ ->
        {acc, repairs}
    end
  end

  defp maybe_add_comma_before_string(acc, pos, repairs, prev_token, in_object, rest) do
    # Special logic for strings that might be keys or values
    if in_object do
      # In object context
      trimmed_acc = String.trim_trailing(acc)

      # Check if this string is likely a key (followed by colon) or value
      # Look ahead to see if there's a colon after this string
      is_likely_key = string_followed_by_colon?(rest)

      case prev_token do
        :string when is_likely_key ->
          # Previous was string (value), this is a key, so we need comma between them
          if String.contains?(trimmed_acc, ":") do
            repair =
              create_repair(
                "added missing comma",
                "Added missing comma between object key-value pairs",
                pos
              )

            {trimmed_acc <> ", ", [repair | repairs]}
          else
            {acc, repairs}
          end

        :number when is_likely_key ->
          # Previous was number (value), this is a key, need comma
          repair =
            create_repair(
              "added missing comma",
              "Added missing comma between object key-value pairs",
              pos
            )

          {trimmed_acc <> ", ", [repair | repairs]}

        _ ->
          {acc, repairs}
      end
    else
      # In array context - use regular logic
      maybe_add_comma_before_value(acc, pos, repairs, prev_token, in_object)
    end
  end

  # Check if a string is followed by a colon (indicating it's a key)
  defp string_followed_by_colon?(content) do
    # Find the end of the string and check if colon follows
    case find_string_end(content, 0, false) do
      {:found, end_pos} ->
        remaining = String.slice(content, end_pos + 1, String.length(content))
        trimmed = String.trim_leading(remaining)
        String.starts_with?(trimmed, ":")

      :not_found ->
        false
    end
  end

  defp find_string_end(content, pos, escaped) do
    case String.at(content, pos) do
      nil -> :not_found
      "\"" when not escaped -> {:found, pos}
      "\\" when not escaped -> find_string_end(content, pos + 1, true)
      _ -> find_string_end(content, pos + 1, false)
    end
  end

  defp consume_number_in_add_commas(content, offset) do
    case String.at(content, offset) do
      nil ->
        {"", offset}

      char
      when char in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ".", "-", "+", "e", "E"] ->
        {rest_number, rest_offset} = consume_number_in_add_commas(content, offset + 1)
        {char <> rest_number, rest_offset}

      _ ->
        {"", offset}
    end
  end

  # Add missing colons - simplified implementation
  defp add_missing_colons(content, repairs) when is_binary(content) do
    state = %{
      acc: "",
      in_string: false,
      escape_next: false,
      quote: nil,
      pos: 0,
      repairs: repairs,
      in_object: false,
      found_key: false
    }

    final_state = add_missing_colons_simple(content, state)
    {final_state.acc, final_state.repairs}
  end

  # Simplified colon addition with struct-based state
  defp add_missing_colons_simple("", state), do: state

  defp add_missing_colons_simple(content, state) do
    <<char::utf8, rest::binary>> = content
    char_str = <<char::utf8>>

    new_state = process_colon_char(char_str, rest, state)
    add_missing_colons_simple(rest, new_state)
  end

  defp process_colon_char(char_str, rest, state) do
    cond do
      state.escape_next ->
        %{state | acc: state.acc <> char_str, escape_next: false, pos: state.pos + 1}

      state.in_string && char_str == "\\" ->
        %{state | acc: state.acc <> char_str, escape_next: true, pos: state.pos + 1}

      state.in_string && char_str == state.quote ->
        new_found_key =
          state.in_object && !String.ends_with?(String.trim_trailing(state.acc), ":")

        %{
          state
          | acc: state.acc <> char_str,
            in_string: false,
            quote: nil,
            found_key: new_found_key,
            pos: state.pos + 1
        }

      state.in_string ->
        %{state | acc: state.acc <> char_str, pos: state.pos + 1}

      char_str == "\"" ->
        %{state | acc: state.acc <> char_str, in_string: true, quote: "\"", pos: state.pos + 1}

      char_str == "{" ->
        %{
          state
          | acc: state.acc <> char_str,
            in_object: true,
            found_key: false,
            pos: state.pos + 1
        }

      char_str == "}" ->
        %{
          state
          | acc: state.acc <> char_str,
            in_object: false,
            found_key: false,
            pos: state.pos + 1
        }

      char_str == ":" ->
        %{state | acc: state.acc <> char_str, found_key: false, pos: state.pos + 1}

      char_str == "," ->
        %{state | acc: state.acc <> char_str, found_key: false, pos: state.pos + 1}

      char_str in [" ", "\t", "\n", "\r"] && state.found_key && state.in_object ->
        handle_whitespace_after_key(char_str, rest, state)

      true ->
        %{state | acc: state.acc <> char_str, pos: state.pos + 1}
    end
  end

  defp handle_whitespace_after_key(char_str, rest, state) do
    trimmed_rest = String.trim_leading(rest)
    is_value_start = is_json_value_start?(trimmed_rest)
    needs_colon = is_value_start && !String.ends_with?(String.trim_trailing(state.acc), ":")

    if needs_colon do
      repair =
        create_repair("added missing colon", "Added missing colon after object key", state.pos)

      %{
        state
        | acc: state.acc <> ":" <> char_str,
          repairs: [repair | state.repairs],
          found_key: false,
          pos: state.pos + 1
      }
    else
      %{state | acc: state.acc <> char_str, pos: state.pos + 1}
    end
  end

  defp is_json_value_start?(str) do
    String.starts_with?(str, "\"") ||
      String.starts_with?(str, "'") ||
      (String.length(str) > 0 &&
         String.at(str, 0) in ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-"]) ||
      String.starts_with?(str, "true") ||
      String.starts_with?(str, "false") ||
      String.starts_with?(str, "null") ||
      String.starts_with?(str, "{") ||
      String.starts_with?(str, "[")
  end

  # Helper functions
  defp is_identifier_start(char) when is_binary(char) do
    # Support ASCII letters, underscore, and UTF-8 characters
    (char >= "a" && char <= "z") ||
      (char >= "A" && char <= "Z") ||
      char == "_" ||
      is_utf8_letter(char)
  end

  defp is_identifier_start(_), do: false

  defp is_identifier_char(char) when is_binary(char) do
    is_identifier_start(char) || (char >= "0" && char <= "9") || char == "$"
  end

  defp is_identifier_char(_), do: false

  # Check if a character is a UTF-8 letter (simplified approach)
  defp is_utf8_letter(char) when is_binary(char) do
    # For UTF-8 characters, we'll be permissive and allow any non-ASCII character
    # that's not a control character or common JSON syntax character
    byte_size(char) > 1 &&
      char != "\"" &&
      char != "'" &&
      char != "{" &&
      char != "}" &&
      char != "[" &&
      char != "]" &&
      char != ":" &&
      char != "," &&
      char != " " &&
      char != "\t" &&
      char != "\n" &&
      char != "\r"
  end

  defp is_number_char(char) do
    (char >= "0" && char <= "9") || char in [".", "-", "+", "e", "E"]
  end

  defp pop_stack_safe([]), do: {[], nil}
  defp pop_stack_safe([head | tail]), do: {tail, head}

  defp determine_next_expecting(state) do
    case List.first(state.context_stack) do
      :object -> :comma_or_end
      :array -> :comma_or_end
      _ -> :value
    end
  end

  defp determine_expecting_after_close(stack) do
    case List.first(stack) do
      :object -> :comma_or_end
      :array -> :comma_or_end
      _ -> :value
    end
  end

  defp create_repair(action, _description, position) do
    %{
      layer: :syntax_normalization,
      action: action,
      position: position,
      original: nil,
      replacement: nil
    }
  end

  # Simple heuristic to detect if content has syntax issues (no regex)
  defp has_syntax_issues?(content) do
    String.contains?(content, "'") ||
      has_unquoted_keys?(content) ||
      String.contains?(content, "True") ||
      String.contains?(content, "False") ||
      String.contains?(content, "TRUE") ||
      String.contains?(content, "FALSE") ||
      String.contains?(content, "None") ||
      String.contains?(content, "NULL") ||
      String.contains?(content, "Null") ||
      has_trailing_commas?(content) ||
      has_missing_commas?(content) ||
      has_missing_colons?(content)
  end

  # Check for unquoted keys using string analysis
  defp has_unquoted_keys?(content) do
    # Look for pattern like: letter followed by colon outside of strings
    check_unquoted_keys(content, false, false, nil, 0)
  end

  defp check_unquoted_keys("", _in_string, _escape_next, _quote, _pos), do: false

  defp check_unquoted_keys(content, in_string, escape_next, quote, pos) do
    <<char::utf8, rest::binary>> = content
    char_str = <<char::utf8>>

    cond do
      escape_next ->
        check_unquoted_keys(rest, in_string, false, quote, pos + 1)

      in_string && char_str == "\\" ->
        check_unquoted_keys(rest, in_string, true, quote, pos + 1)

      in_string && char_str == quote ->
        check_unquoted_keys(rest, false, false, nil, pos + 1)

      in_string ->
        check_unquoted_keys(rest, in_string, escape_next, quote, pos + 1)

      char_str == "\"" ->
        check_unquoted_keys(rest, true, false, "\"", pos + 1)

      !in_string && is_identifier_start(char_str) ->
        # Found start of identifier outside string, check if it's followed by colon
        case find_colon_after_identifier(content, pos) do
          {:found, _colon_pos} -> true
          :not_found -> check_unquoted_keys(rest, false, false, nil, pos + 1)
        end

      true ->
        check_unquoted_keys(rest, false, false, nil, pos + 1)
    end
  end

  # Look for colon after identifier to detect unquoted keys
  defp find_colon_after_identifier(content, start_pos) do
    {_identifier, chars_consumed} = consume_identifier(content, start_pos)

    remaining = String.slice(content, start_pos + chars_consumed, String.length(content))
    trimmed = String.trim_leading(remaining)

    if String.starts_with?(trimmed, ":") do
      {:found, start_pos + chars_consumed + (String.length(remaining) - String.length(trimmed))}
    else
      :not_found
    end
  end

  # Check for trailing commas
  defp has_trailing_commas?(content) do
    String.contains?(content, ",}") || String.contains?(content, ",]")
  end

  # Check for missing commas (simplified detection)
  defp has_missing_commas?(content) do
    # Look for patterns like: value followed by value without comma
    # object value followed by key
    # string value followed by key
    String.contains?(content, "\" \"") ||
      String.contains?(content, "} {") ||
      String.contains?(content, "] [") ||
      String.contains?(content, "\": 1 \"") ||
      String.contains?(content, "\": \"Alice\" \"") ||
      has_number_sequence?(content)
  end

  # Check for missing colons
  defp has_missing_colons?(content) do
    # Look for patterns like: "key" "value" or key "value"
    String.contains?(content, "\" \"") && !String.contains?(content, "\": \"")
  end

  # Check for number sequences without commas
  defp has_number_sequence?(content) do
    check_number_sequence(content, false, false, nil, 0, false)
  end

  defp check_number_sequence("", _in_string, _escape_next, _quote, _pos, _found_number), do: false

  defp check_number_sequence(content, in_string, escape_next, quote, pos, found_number) do
    <<char::utf8, rest::binary>> = content
    char_str = <<char::utf8>>

    cond do
      escape_next ->
        check_number_sequence(rest, in_string, false, quote, pos + 1, found_number)

      in_string && char_str == "\\" ->
        check_number_sequence(rest, in_string, true, quote, pos + 1, found_number)

      in_string && char_str == quote ->
        check_number_sequence(rest, false, false, nil, pos + 1, false)

      in_string ->
        check_number_sequence(rest, in_string, escape_next, quote, pos + 1, found_number)

      char_str == "\"" ->
        check_number_sequence(rest, true, false, "\"", pos + 1, false)

      !in_string && char_str >= "0" && char_str <= "9" ->
        if found_number do
          # Found second number after previous one - check if there's a comma between
          true
        else
          check_number_sequence(rest, false, false, nil, pos + 1, true)
        end

      !in_string && char_str in [" ", "\t", "\n", "\r"] ->
        # Whitespace - continue with same found_number state
        check_number_sequence(rest, false, false, nil, pos + 1, found_number)

      !in_string && char_str == "," ->
        # Found comma - reset number state
        check_number_sequence(rest, false, false, nil, pos + 1, false)

      !in_string ->
        # Other character - reset number state
        check_number_sequence(rest, false, false, nil, pos + 1, false)

      true ->
        check_number_sequence(rest, in_string, escape_next, quote, pos + 1, found_number)
    end
  end
end
</file>

<file path="json_remedy/utils/char_utils.ex">
defmodule JsonRemedy.Utils.CharUtils do
  @moduledoc """
  Optimized UTF-8 safe character navigation utilities for JSON parsing.

  This module provides high-performance character-by-character navigation functions that handle
  Unicode characters correctly while maintaining excellent performance for JSON string processing.
  All functions use String.length/1 and String.at/2 for UTF-8 safety, avoiding byte-based
  operations that could break on multi-byte characters.

  ## Performance Optimizations

  - Early bounds checking to avoid unnecessary computations
  - Optimized whitespace detection using compile-time constants
  - Tail-recursive functions for memory efficiency
  - Minimal pattern matching overhead
  - Reduced function call overhead in hot paths

  ## Features

  - UTF-8 safe character positioning using grapheme-based counting
  - Comprehensive bounds checking with graceful error handling
  - Defensive programming patterns against nil and invalid inputs
  - Context-aware whitespace handling for JSON parsing scenarios
  - Performance-optimized for common character navigation tasks

  ## Safety Guarantees

  All functions in this module:
  - Handle nil inputs gracefully without raising exceptions
  - Use String.length/1 for character counting (not byte_size/1)
  - Use String.at/2 for character access (not binary pattern matching)
  - Return consistent result types for error conditions
  - Preserve UTF-8 character boundaries correctly

  ## Examples

      iex> JsonRemedy.Utils.CharUtils.get_char_at("café", 3, nil)
      "é"

      iex> JsonRemedy.Utils.CharUtils.skip_to_character("hello world", "w", 0)
      6

      iex> JsonRemedy.Utils.CharUtils.whitespace?(" ")
      true

      iex> JsonRemedy.Utils.CharUtils.char_at_position_safe("test", 10)
      nil
  """

  # Compile-time optimization: define whitespace characters as module attribute
  @whitespace_chars [" ", "\t", "\n", "\r", "\f", "\v"]

  @type position :: non_neg_integer()
  @type char_result :: String.t() | nil
  @type search_result :: non_neg_integer() | nil

  @doc """
  Safely retrieves character at specified position with default fallback.

  Optimized for performance with early bounds checking and minimal overhead.
  Returns the character at the given position, or the default value if the position
  is out of bounds, the input is nil, or any other error condition occurs.

  ## Parameters

  - `input` - The input string (may be nil)
  - `position` - Zero-based character position (non-negative integer)
  - `default` - Value to return if character cannot be retrieved

  ## Returns

  - The character at the position if valid
  - The default value if position is out of bounds or input is nil/invalid

  ## Examples

      iex> get_char_at("hello", 0, nil)
      "h"

      iex> get_char_at("café", 3, nil)
      "é"

      iex> get_char_at("test", 10, "?")
      "?"

      iex> get_char_at(nil, 0, "default")
      "default"

      iex> get_char_at("", 0, nil)
      nil
  """
  @spec get_char_at(String.t() | nil, position(), any()) :: any()
  def get_char_at(input, position, default)
      when is_binary(input) and is_integer(position) and position >= 0 do
    # Optimization: Use String.at/2 directly since it already handles bounds checking
    String.at(input, position) || default
  end

  def get_char_at(_input, _position, default), do: default

  @doc """
  Searches for target character starting from given position.

  Optimized with early bounds checking and efficient search loop.
  Finds the first occurrence of the target character in the input string, starting
  the search from the specified position. Returns the position where the character
  was found, or nil if not found or invalid input.

  ## Parameters

  - `input` - The input string to search (may be nil)
  - `target_char` - The character to search for (may be nil)
  - `start_pos` - Starting position for the search (non-negative integer)

  ## Returns

  - Position (integer) where the character was found
  - `nil` if character not found, input is nil, or invalid parameters

  ## Examples

      iex> skip_to_character("hello world", "w", 0)
      6

      iex> skip_to_character("test", "s", 2)
      2

      iex> skip_to_character("café résumé", "é", 0)
      3

      iex> skip_to_character("hello", "x", 0)
      nil

      iex> skip_to_character(nil, "a", 0)
      nil
  """
  @spec skip_to_character(String.t() | nil, String.t() | nil, position()) :: search_result()
  def skip_to_character(input, target_char, start_pos)
      when is_binary(input) and is_binary(target_char) and is_integer(start_pos) and
             start_pos >= 0 do
    input_length = String.length(input)

    # Optimization: Early bounds checking
    if start_pos >= input_length do
      nil
    else
      # Optimization: Pass input_length to avoid recalculating
      do_find_character(input, target_char, start_pos, input_length)
    end
  end

  def skip_to_character(_input, _target_char, _start_pos), do: nil

  @doc """
  Skips whitespace characters from start position up to end position.

  Highly optimized with compile-time whitespace constants and efficient loop.
  Advances through the input string starting from start_pos, skipping over whitespace
  characters until a non-whitespace character is found or end_pos is reached.

  ## Parameters

  - `input` - The input string (may be nil)
  - `start_pos` - Starting position (non-negative integer)
  - `end_pos` - Maximum position to check (non-negative integer)

  ## Returns

  - Position (integer) of first non-whitespace character or end_pos if all whitespace
  - start_pos if input is nil or invalid parameters

  ## Examples

      iex> skip_whitespaces_at("   hello", 0, 8)
      3

      iex> skip_whitespaces_at("hello", 0, 5)
      0

      iex> skip_whitespaces_at("  \\t\\n  test", 0, 12)
      6

      iex> skip_whitespaces_at("   ", 0, 3)
      3

      iex> skip_whitespaces_at(nil, 0, 5)
      0
  """
  @spec skip_whitespaces_at(String.t() | nil, position(), position()) :: position()
  def skip_whitespaces_at(input, start_pos, end_pos)
      when is_binary(input) and is_integer(start_pos) and is_integer(end_pos) and
             start_pos >= 0 and end_pos >= 0 do
    # Optimization: Calculate bounds once
    input_length = String.length(input)
    actual_end = min(end_pos, input_length)

    # Optimization: Early return if start position is already at or beyond end
    if start_pos >= actual_end do
      start_pos
    else
      do_skip_whitespace(input, start_pos, actual_end)
    end
  end

  def skip_whitespaces_at(_input, start_pos, _end_pos)
      when is_integer(start_pos) and start_pos >= 0,
      do: start_pos

  def skip_whitespaces_at(_input, _start_pos, _end_pos), do: 0

  @doc """
  Checks if a character is considered whitespace for JSON parsing.

  Optimized with compile-time constant for maximum performance.
  Determines whether the given character should be treated as whitespace in the
  context of JSON parsing. Handles both ASCII and Unicode whitespace characters.

  ## Parameters

  - `char` - The character to check (may be nil or non-string)

  ## Returns

  - `true` if the character is whitespace
  - `false` if the character is not whitespace or is nil/invalid

  ## Examples

      iex> whitespace?(" ")
      true

      iex> whitespace?("\\t")
      true

      iex> whitespace?("\\n")
      true

      iex> whitespace?("\\r")
      true

      iex> whitespace?("a")
      false

      iex> whitespace?(nil)
      false

      iex> whitespace?("")
      false
  """
  @spec whitespace?(String.t() | nil) :: boolean()
  def whitespace?(char) when is_binary(char) do
    # Optimization: Use compile-time module attribute instead of inline list
    char in @whitespace_chars
  end

  def whitespace?(_char), do: false

  @doc """
  Safe wrapper for String.at/2 that handles nil inputs gracefully.

  Optimized thin wrapper that leverages String.at/2's built-in bounds checking.
  Provides a safe way to access characters at specific positions without raising
  exceptions. Returns nil for any error condition.

  ## Parameters

  - `input` - The input string (may be nil)
  - `position` - Zero-based character position (non-negative integer)

  ## Returns

  - The character at the position if valid
  - `nil` if position is out of bounds, input is nil, or invalid parameters

  ## Examples

      iex> char_at_position_safe("hello", 1)
      "e"

      iex> char_at_position_safe("café", 3)
      "é"

      iex> char_at_position_safe("test", 10)
      nil

      iex> char_at_position_safe("", 0)
      nil

      iex> char_at_position_safe(nil, 0)
      nil
  """
  @spec char_at_position_safe(String.t() | nil, position()) :: char_result()
  def char_at_position_safe(input, position)
      when is_binary(input) and is_integer(position) and position >= 0 do
    # Optimization: String.at/2 already handles bounds checking efficiently
    String.at(input, position)
  end

  def char_at_position_safe(_input, _position), do: nil

  # Private optimized helper functions

  # Optimized character search with tail recursion
  @spec do_find_character(String.t(), String.t(), position(), position()) :: search_result()
  defp do_find_character(input, target_char, pos, input_length) when pos < input_length do
    case String.at(input, pos) do
      ^target_char -> pos
      _ -> do_find_character(input, target_char, pos + 1, input_length)
    end
  end

  defp do_find_character(_input, _target_char, _pos, _input_length), do: nil

  # Optimized whitespace skipping with compile-time constants
  @spec do_skip_whitespace(String.t(), position(), position()) :: position()
  defp do_skip_whitespace(input, pos, end_pos) when pos < end_pos do
    case String.at(input, pos) do
      char when char in @whitespace_chars ->
        do_skip_whitespace(input, pos + 1, end_pos)

      _ ->
        pos
    end
  end

  defp do_skip_whitespace(_input, pos, _end_pos), do: pos
end
</file>

<file path="json_remedy/layer_behaviour.ex">
defmodule JsonRemedy.LayerBehaviour do
  @moduledoc """
  Defines the contract that all repair layers must implement.

  Each layer is responsible for one specific type of repair concern
  and should be composable with other layers in the pipeline.
  """

  @type repair_action :: %{
          layer: atom(),
          action: String.t(),
          position: non_neg_integer() | nil,
          original: String.t() | nil,
          replacement: String.t() | nil
        }

  @type repair_context :: %{
          repairs: [repair_action()],
          options: keyword(),
          metadata: map()
        }

  @type layer_result ::
          {:ok, String.t(), repair_context()}
          | {:continue, String.t(), repair_context()}
          | {:error, String.t()}

  @doc """
  Process input string and apply layer-specific repairs.

  Returns:
  - `{:ok, processed_input, updated_context}` - Layer completed successfully
  - `{:continue, input, context}` - Layer doesn't apply, pass to next layer
  - `{:error, reason}` - Layer failed, stop pipeline
  """
  @callback process(input :: String.t(), context :: repair_context()) :: layer_result()

  @doc """
  Check if this layer can handle the given input.
  Used for optimization and layer selection.
  """
  @callback supports?(input :: String.t()) :: boolean()

  @doc """
  Return the priority order for this layer (lower = earlier).
  Used to determine layer execution order.
  """
  @callback priority() :: non_neg_integer()

  @doc """
  Return a human-readable name for this layer.
  Used in logging and debugging.
  """
  @callback name() :: String.t()

  @doc """
  Validate layer configuration and options.
  Called during pipeline setup.
  """
  @callback validate_options(options :: keyword()) :: :ok | {:error, String.t()}

  @optional_callbacks validate_options: 1

  @doc """
  Check if a position in the input is inside a string literal.
  Used to avoid applying repairs to string content.
  """
  @spec inside_string?(input :: String.t(), position :: non_neg_integer()) :: boolean()
  def inside_string?(input, position) do
    before = String.slice(input, 0, position)

    # Count unescaped quotes before this position using string processing
    quote_count = count_unescaped_quotes(before, 0, 0)

    # Odd number means we're inside a string
    rem(quote_count, 2) != 0
  end

  # Helper function to count unescaped quotes without regex
  defp count_unescaped_quotes("", _pos, count), do: count

  defp count_unescaped_quotes(<<"\\\"", rest::binary>>, pos, count) do
    # Skip escaped quote
    count_unescaped_quotes(rest, pos + 2, count)
  end

  defp count_unescaped_quotes(<<"\"", rest::binary>>, pos, count) do
    # Found unescaped quote
    count_unescaped_quotes(rest, pos + 1, count + 1)
  end

  defp count_unescaped_quotes(<<_char, rest::binary>>, pos, count) do
    # Regular character
    count_unescaped_quotes(rest, pos + 1, count)
  end
end
</file>

</files>
