# encoding: ascii-8bit

# Copyright 2021 Ball Aerospace & Technologies Corp.
# All Rights Reserved.
#
# This program is free software; you can modify and/or redistribute it
# under the terms of the GNU Affero General Public License
# as published by the Free Software Foundation; version 3 with
# attribution addendums as found in the LICENSE.txt
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# This program may also be used under the terms of a commercial or
# enterprise edition license of COSMOS if purchased from the
# copyright holder

require 'cosmos'
require 'tempfile'
require 'cosmos/utilities/s3'
Cosmos.require_file 'cosmos/utilities/store'
Cosmos.require_file 'cosmos/tools/table_manager/table_manager_core'

class Table
  DEFAULT_BUCKET_NAME = 'config'

  def self.all(scope)
    rubys3_client = Aws::S3::Client.new
    resp = rubys3_client.list_objects_v2(bucket: DEFAULT_BUCKET_NAME)
    result = []
    modified = []
    contents = resp.to_h[:contents]
    if contents
      contents.each do |object|
        next unless object[:key].include?("#{scope}/targets")

        if object[:key].include?('tables')
          if object[:key].include?("#{scope}/targets_modified")
            modified << object[:key].split('/')[2..-1].join('/')
            next
          end
          result << object[:key].split('/')[2..-1].join('/')
        end
      end
    end

    # Determine if there are any modified files and mark them with '*'
    result.map! do |file|
      if modified.include?(file)
        modified.delete(file)
        "#{file}*"
      else
        file
      end
    end

    # Concat any remaining modified files (new files not in original target)
    result.concat(modified)
    result.sort
  end

  def self.body(scope, name)
    get_file(scope, name)
  end

  def self.binary(scope, binary_filename, definition_filename = nil, table_name = nil)
    binary = OpenStruct.new
    binary.filename = File.basename(binary_filename)
    binary.contents = get_file(scope, binary_filename)
    if definition_filename && table_name
      root_definition = get_definitions(scope, definition_filename)
      # Convert the typical table naming convention of all caps with underscores
      # to the typical binary convention of camelcase, e.g. MC_CONFIG => McConfig.bin
      filename = table_name.split('_').map { |part| part.capitalize }.join()
      binary.filename = "#{filename}.bin"
      binary.contents = Cosmos::TableManagerCore.binary(binary.contents, root_definition, table_name)
    end
    return binary
  end

  def self.definition(scope, definition_filename, table_name = nil)
    definition = OpenStruct.new
    if table_name
      root_definition = get_definitions(scope, definition_filename)
      definition.filename, definition.contents =
        Cosmos::TableManagerCore.definition(root_definition, table_name)
    else
      definition.filename = File.basename(definition_filename)
      definition.contents = get_file(scope, definition_filename)
    end
    return definition
  end

  def self.report(scope, binary_filename, definition_filename, table_name = nil)
    report = OpenStruct.new
    binary = get_file(scope, binary_filename)
    root_definition = get_definitions(scope, definition_filename)
    if table_name
      # Convert the typical table naming convention of all caps with underscores
      # to the typical binary convention of camelcase, e.g. MC_CONFIG => McConfig.bin
      filename = table_name.split('_').map { |part| part.capitalize }.join()
      report.filename = "#{filename}.csv"
    else
      report.filename = File.basename(binary_filename).sub('.bin', '.csv')
    end
    report.contents = Cosmos::TableManagerCore.report(binary, root_definition, table_name)
    put_file(scope, binary_filename.sub('.bin', '.csv'), report.contents)
    return report
  end

  def self.load(scope, binary_filename, definition_filename)
    binary = get_file(scope, binary_filename)
    root_definition = get_definitions(scope, definition_filename)
    return Cosmos::TableManagerCore.build_json(binary, root_definition)
  end

  def self.save(scope, binary_filename, definition_filename, tables)
    binary = get_file(scope, binary_filename)
    raise "Binary file '#{binary_filename}' not found" unless binary
    root_definition = get_definitions(scope, definition_filename)
    binary = Cosmos::TableManagerCore.save(root_definition, JSON.parse(tables))
    put_file(scope, binary_filename, binary)
  end

  def self.save_as(scope, filename, new_filename)
    file = get_file(scope, filename)
    raise "File '#{filename}' not found" unless file
    put_file(scope, new_filename, file)
  end

  def self.generate(scope, definition_filename)
    root_definition = get_definitions(scope, definition_filename)
    binary = Cosmos::TableManagerCore.generate(root_definition)
    binary_filename = "#{File.dirname(definition_filename).sub('/config','/bin')}/#{File.basename(definition_filename)}"
    binary_filename.sub!('_def', '') # Strip off _def from the definition filename
    binary_filename.sub!('.txt', '.bin')
    put_file(scope, binary_filename, binary)
    return binary_filename
  end

  def self.destroy(scope, name)
    # Only delete file from the modified target directory
    Aws::S3::Client.new.delete_object(
      key: "#{scope}/targets_modified/#{name}",
      bucket: DEFAULT_BUCKET_NAME,
    )
    true
  end

  def self.lock(scope, name, user)
    name = name.split('*')[0] # Split '*' that indicates modified
    Cosmos::Store.hset("#{scope}__table-locks", name, user)
  end

  def self.unlock(scope, name)
    name = name.split('*')[0] # Split '*' that indicates modified
    Cosmos::Store.hdel("#{scope}__table-locks", name)
  end

  def self.locked?(scope, name)
    name = name.split('*')[0] # Split '*' that indicates modified
    locked_by = Cosmos::Store.hget("#{scope}__table-locks", name)
    locked_by ||= false
    locked_by
  end

  # Private helper methods

  def self.get_definitions(scope, definition_filename)
    temp_dir = Dir.mktmpdir
    definition = get_file(scope, definition_filename)
    base_definition = File.join(temp_dir, File.basename(definition_filename))
    File.write(base_definition, definition)
    # If the definition includes TABLEFILE we need to load
    # the other definitions locally so we can render them
    base_dir = File.dirname(definition_filename)
    definition.split("\n").each do |line|
      if line.strip =~ /^TABLEFILE (.*)/
        filename = File.join(base_dir, $1.remove_quotes)
        file = get_file(scope, filename)
        raise "Could not find file #{filename}" unless file
        File.write(File.join(temp_dir, File.basename(filename)), file)
      end
    end
    base_definition
  end

  def self.get_file(scope, name)
    name = name.split('*')[0] # Split '*' that indicates modified
    rubys3_client = Aws::S3::Client.new
    begin
      # First try opening a potentially modified version by looking for the modified target
      resp =
        rubys3_client.get_object(
          bucket: DEFAULT_BUCKET_NAME,
          key: "#{scope}/targets_modified/#{name}",
        )
    rescue Aws::S3::Errors::NoSuchKey
      begin
        # Now try the original
        resp =
          rubys3_client.get_object(
            bucket: DEFAULT_BUCKET_NAME,
            key: "#{scope}/targets/#{name}",
          )
      rescue Aws::S3::Errors::NoSuchKey
        return nil
      end
    end
    if name.include?(".bin")
      resp.body.binmode
    end
    resp.body.read
  end

  def self.put_file(scope, name, data)
    rubys3_client = Aws::S3::Client.new
    key = "#{scope}/targets_modified/#{name}"
    rubys3_client.put_object(bucket: DEFAULT_BUCKET_NAME, key: key, body: data)
  end
end
