# prompt_manager/lib/prompt_manager/storage/file_system_adapter.rb

# Use the local (or remote) file system as a place to
# store and access prompts.
#
# Adds two additional methods to the Promp class:
#   list - returns Array of prompt IDs
#   path = returns a Pathname object to the prompt's text file
#   path(prompt_id) - same as path on the prompt instance
#
# Allows sub-directories of the prompts_dir to be
# used like categories.  For example the prompt_id "toy/magic"
# is found in the `magic.txt` file inside the `toy` sub-directory
# of the prompts_dir.
#
# There can man be many layers of categories (sub-directories)
#

require 'json'      # basic serialization of parameters
require 'pathname'

class PromptManager::Storage::FileSystemAdapter
  SEARCH_PROC       = nil # placeholder
  PARAMS_EXTENSION  = '.json'.freeze
  PROMPT_EXTENSION  = '.txt'.freeze
  PROMPT_ID_FORMAT  = /^[a-zA-Z0-9\-\/_]+$/

  class << self
    attr_accessor :prompts_dir, :search_proc, 
                  :params_extension, :prompt_extension
  
    def config
      if block_given?
        yield self
        validate_configuration     
      else
        raise ArgumentError, "No block given to config"
      end

      self
    end

    # Expansion methods on the Prompt class specific to
    # this storage adapter.

    # Ignore the incoming prompt_id
    def list(prompt_id = nil)
      new.list
    end


    def path(prompt_id)
      new.path(prompt_id)
    end

    #################################################
    private

    def validate_configuration
      validate_prompts_dir
      validate_search_proc
      validate_prompt_extension
      validate_params_extension
    end


    def validate_prompts_dir
      # This is a work around for a Ruby scope issue where the 
      # class getter/setter method is becoming confused with a 
      # local variable when anything other than plain 'ol get and 
      # set are used.'This error is in both Ruby v3.2.2 and
      # v3.3.0-preview3.
      #
      prompts_dir_local = self.prompts_dir

      unless prompts_dir_local.is_a?(Pathname)
        prompts_dir_local = Pathname.new(prompts_dir_local) unless prompts_dir_local.nil?
      end

      prompts_dir_local = prompts_dir_local.expand_path

      raise(ArgumentError, "prompts_dir: #{prompts_dir_local}") unless prompts_dir_local.exist? && prompts_dir_local.directory?
      
      self.prompts_dir = prompts_dir_local
    end


    def validate_search_proc
      search_proc_local = self.search_proc

      if search_proc_local.nil?
        search_proc_local = SEARCH_PROC
      else
        raise(ArgumentError, "search_proc invalid; does not respond to call") unless search_proc_local.respond_to?(:call)
      end

      self.search_proc = search_proc_local
    end


    def validate_prompt_extension
      prompt_extension_local = self.prompt_extension

      if prompt_extension_local.nil?
        prompt_extension_local = PROMPT_EXTENSION
      else
        unless  prompt_extension_local.is_a?(String)    &&
                prompt_extension_local.start_with?('.')
          raise(ArgumentError, "Invalid prompt_extension: #{prompt_extension_local}")
        end
      end

      self.prompt_extension = prompt_extension_local
    end


    def validate_params_extension
      params_extension_local = self.params_extension

      if params_extension_local.nil?
        params_extension_local = PARAMS_EXTENSION
      else
        unless  params_extension_local.is_a?(String)    &&
                params_extension_local.start_with?('.')
          raise(ArgumentError, "Invalid params_extension: #{params_extension_local}")
        end
      end

      self.params_extension = params_extension_local
    end
  end


  ##################################################
  ###
  ##  Instance
  #

  def prompts_dir       = self.class.prompts_dir
  def search_proc       = self.class.search_proc
  def prompt_extension  = self.class.prompt_extension
  def params_extension  = self.class.params_extension


  def initialize
    # NOTE: validate because main program may have made
    #       changes outside of the config block
    self.class.send(:validate_configuration) # send gets around private designations of a method
  end


  def get(id:)
    validate_id(id)
    verify_id(id)

    {
      id:         id,
      text:       prompt_text(id),
      parameters: parameter_values(id)
    }
  end


  # Retrieve prompt text by its id
  def prompt_text(prompt_id)
    read_file(file_path(prompt_id, prompt_extension))
  end


  # Retrieve parameter values by its id
  def parameter_values(prompt_id)
    params_path = file_path(prompt_id, params_extension)
    
    if params_path.exist?
      parms_content = read_file(params_path)
      deserialize(parms_content)
    else
      {}
    end
  end


  # Save prompt text and parameter values to corresponding files
  def save(
      id:, 
      text: "", 
      parameters: {}
    )
    validate_id(id)

    prompt_filepath = file_path(id, prompt_extension)
    params_filepath = file_path(id, params_extension)
    
    write_with_error_handling(prompt_filepath, text)
    write_with_error_handling(params_filepath, serialize(parameters))
  end


  # Delete prompted text and parameter values files
  def delete(id:)
    validate_id(id)

    prompt_filepath = file_path(id, prompt_extension)
    params_filepath = file_path(id, params_extension)
    
    delete_with_error_handling(prompt_filepath)
    delete_with_error_handling(params_filepath)
  end


  def search(for_what)
    search_term = for_what.downcase

    if search_proc.is_a? Proc
      search_proc.call(search_term)
    else
      search_prompts(search_term)
    end
  end


  # Return an Array of prompt IDs
  def list(*)
    prompt_ids = []
    
    Pathname.glob(prompts_dir.join("**/*#{prompt_extension}")).each do |file_path|
      prompt_id = file_path.relative_path_from(prompts_dir).to_s.gsub(prompt_extension, '')
      prompt_ids << prompt_id
    end

    prompt_ids
  end


  # Returns a Pathname object for a prompt ID text file
  # However, it is possible that the file does not exist.
  def path(id)
    validate_id(id)
    file_path(id, prompt_extension) 
  end

  ##########################################
  private

  # Validate that the ID contains good characters.
  def validate_id(id)
    raise ArgumentError, "Invalid ID format id: #{id}" unless id =~ PROMPT_ID_FORMAT
  end


  def verify_id(id)
    unless file_path(id, prompt_extension).exist?
      raise ArgumentError, "Invalid prompt_id: #{id}"
    end
  end


  def write_with_error_handling(file_path, content)
    begin
      file_path.write content
      true
    rescue IOError => e
      raise "Failed to write to file: #{e.message}"
    end
  end


  # file_path (Pathname)
  def delete_with_error_handling(file_path)
    begin
      file_path.delete
      true
    rescue IOError => e
      raise "Failed to delete file: #{e.message}"
    end
  end


  def file_path(id, extension)
    prompts_dir + "#{id}#{extension}"
  end


  def read_file(full_path)
    raise IOError, 'File does not exist' unless File.exist?(full_path)
    File.read(full_path)
  end


  def search_prompts(search_term)
    prompt_ids = []
    
    Pathname.glob(prompts_dir.join("**/*#{prompt_extension}")).each do |prompt_path|
      if prompt_path.read.downcase.include?(search_term)
        prompt_id = prompt_path.relative_path_from(prompts_dir).to_s.gsub(prompt_extension, '')    
        prompt_ids << prompt_id
      end
    end

    prompt_ids
  end


  # TODO: Should the serializer be generic?

  def serialize(data)
    data.to_json
  end


  def deserialize(data)
    JSON.parse(data)
  end
end