# encoding: utf-8
# frozen_string_literal: true

require 'rchardet'
require 'zip'

module ActiveAdminImport
  class Model
    include ActiveModel::Model
    include ActiveModel::Validations
    include ActiveModel::Validations::Callbacks

    module CONST
      ZIP_TYPE = 'application/zip'.freeze
      TMP_FILE = 'active-admin-import-unzipped'.freeze
      CSV_TYPES = %w(
        text/csv
        text/x-csv
        text/x-comma-separated-values
        text/comma-separated-values
        application/csv
        application/vnd.ms-excel
        application/vnd.msexcel
        application/octet-stream
        text/tsv
        text/x-tsv
        text/tab-separated-values
        text/x-tab-separated-values
      ).freeze
    end

    validates :file, presence: {
      message: ->(*_) { I18n.t('active_admin_import.no_file_error') }
    }, unless: ->(me) { me.new_record? }

    validate :correct_content_type, if: ->(me) { me.file.present? }
    validate :file_contents_present, if: ->(me) { me.file.present? }

    before_validation :unzip_file, if: ->(me) { me.archive? && me.allow_archive? }
    before_validation :encode_file, if: ->(me) { me.force_encoding? && me.file.present? }

    attr_reader :attributes

    def initialize(args = {})
      @new_record = true
      @attributes = {}
      assign_attributes(default_attributes.merge(args), true)
    end

    def assign_attributes(args = {}, new_record = false)
      @attributes.merge!(args)
      @new_record = new_record
      args.keys.each do |key|
        define_methods_for(key.to_sym)
      end if args.is_a?(Hash)
    end

    def read_attribute_for_validation(key)
      @attributes[key.to_sym]
    end

    def default_attributes
      {
        allow_archive: true,
        csv_headers: [],
        file: nil,
        force_encoding: 'UTF-8',
        hint: ''
      }
    end

    def allow_archive?
      attributes[:allow_archive].present?
    end

    def new_record?
      @new_record.present?
    end

    def force_encoding?
      attributes[:force_encoding].present?
    end

    def persisted?
      false
    end

    def archive?
      file_type == CONST::ZIP_TYPE
    end

    alias to_hash attributes

    protected

    def file_path
      if file.is_a? ActionDispatch::Http::UploadedFile
        file.tempfile.path
      else
        file.path
      end
    end

    def encode_file
      data = File.read(file_path)
      return if data.empty?

      File.open(file_path, 'w') do |f|
        f.write(encode(data))
      end
    end

    def unzip_file
      Zip::File.open(file_path) do |zip_file|
        self.file = Tempfile.new(CONST::TMP_FILE, binmode: true)
        data = zip_file.entries.select(&:file?).first.get_input_stream.read
        file << data
        file.close
      end
    end

    def csv_allowed_types
      CONST::CSV_TYPES
    end

    def correct_content_type
      return if file.blank? ||
          file.is_a?(Tempfile) ||
          (csv_allowed_types.include? file_type)
      errors.add(:file, I18n.t('active_admin_import.file_format_error'))
    end

    def file_contents_present
      return unless File.zero?(file_path)
      errors.add(:file, I18n.t('active_admin_import.file_empty_error'))
    end

    def file_type
      if file.is_a? ActionDispatch::Http::UploadedFile
        file.content_type.chomp
      else
        ''
      end
    end

    def define_methods_for(attr_name)
      # generate methods for instance object by attributes
      singleton_class.class_eval do
        define_set_method(attr_name)
        define_get_method(attr_name)
      end
    end

    def encode(data)
      data = content_encode(data) if force_encoding?
      data = data.encode(
        'UTF-8',
        invalid: :replace, undef: :replace, universal_newline: true
      )
      begin
        data.sub("\xEF\xBB\xBF", '') # bom
      rescue StandardError => _
        data
      end
    end

    def detect_encoding?
      force_encoding == :auto
    end

    def dynamic_encoding(data)
      CharDet.detect(data)['encoding']
    end

    def content_encode(data)
      encoding_name = if detect_encoding?
                        dynamic_encoding(data)
                      else
                        force_encoding.to_s
                      end
      data.force_encoding(encoding_name)
    end

    class <<self
      def define_set_method(attr_name)
        return if method_defined? attr_name
        define_method(attr_name) { attributes[attr_name] }
      end

      def define_get_method(attr_name)
        return if method_defined? "#{attr_name}="
        define_method("#{attr_name}=") { |new_value| @attributes[attr_name] = new_value }
      end
    end
  end
end
