# frozen_string_literal: true

module ImportCsv
  class BaseService
    include Gitlab::Utils::StrongMemoize
    include Gitlab::Allowable

    # The largest CSV export (Work Items EE) has 26 columns.
    # 50 provides headroom for future additions while bounding
    # per-row memory allocation from malicious files.
    MAX_COLUMNS = 50
    # The longest known header line (~26 columns) is under 300 bytes.
    # 2KB provides ample headroom while rejecting multi-MB malicious
    # headers before any CSV parsing occurs.
    MAX_HEADER_LINE_BYTES = 2.kilobytes

    def initialize(user, project, csv_io)
      @user = user
      @project = project
      @csv_io = csv_io
      @results = { success: 0, error_lines: [], parse_error: false, preprocess_errors: {} }
    end

    PreprocessError = Class.new(StandardError)

    def execute
      process_csv
      email_results_to_user

      results
    end

    def email_results_to_user
      raise NotImplementedError
    end

    private

    attr_reader :user, :project, :csv_io, :results
    alias_method :current_user, :user

    def attributes_for(row)
      raise NotImplementedError
    end

    def validate_headers_presence!(headers)
      raise NotImplementedError
    end

    def create_object_class
      raise NotImplementedError
    end

    def validate_structure!
      header_line = csv_data.lines.first
      raise CSV::MalformedCSVError.new('File is empty, no headers found', 1) if header_line.blank?

      validate_column_count!(header_line)
      validate_headers_presence!(header_line)
    end

    def validate_column_count!(header_line)
      if header_line.bytesize > MAX_HEADER_LINE_BYTES
        raise CSV::MalformedCSVError.new(
          "CSV header is too large (#{header_line.bytesize} bytes, maximum is #{MAX_HEADER_LINE_BYTES})", 1
        )
      end

      columns = CSV.parse_line(header_line, col_sep: detect_col_sep)
      column_count = columns.size
      return unless column_count > MAX_COLUMNS

      raise CSV::MalformedCSVError.new(
        "CSV file contains too many columns (#{column_count}, maximum is #{MAX_COLUMNS})", 1
      )
    end

    def preprocess!
      # any logic can be added in subclasses if needed
      # hence just a no-op rather than NotImplementedError
    end

    def process_csv
      validate_structure!
      preprocess!

      with_csv_lines.each do |row, line_no|
        attributes = attributes_for(row)

        if create_object(attributes)&.persisted?
          results[:success] += 1
        else
          results[:error_lines].push(line_no)
        end
      end
    rescue ArgumentError, CSV::MalformedCSVError => e
      results[:parse_error] = true
      results[:error_lines].push(e.line_number) if e.respond_to?(:line_number)
    rescue PreprocessError
      results[:parse_error] = false
    end

    def with_csv_lines
      CSV.new(
        csv_data,
        col_sep: detect_col_sep,
        headers: true,
        header_converters: :symbol
      ).each.with_index(2)
    end

    def csv_data
      @csv_io.open(&:read).force_encoding(Encoding::UTF_8)
    end
    strong_memoize_attr :csv_data

    def detect_col_sep
      header = csv_data.lines.first

      if header.include?(",")
        ","
      elsif header.include?(";")
        ";"
      elsif header.include?("\t")
        "\t"
      else
        raise CSV::MalformedCSVError.new('Invalid CSV format', 1)
      end
    end
    strong_memoize_attr :detect_col_sep

    def create_object(attributes)
      # default_params can be extracted into a method if we need
      # to support creation of objects that belongs to groups.
      default_params = { container: project,
                         current_user: user,
                         params: attributes,
                         perform_spam_check: false }

      create_service = create_object_class.new(**default_params.merge(extra_create_service_params))

      create_service.execute_without_rate_limiting
    end

    # Overidden in subclasses to support specific parameters
    def extra_create_service_params
      {}
    end
  end
end
