require "scaffolding"
require "scaffolding/transformer"
require "scaffolding/block_manipulator"
require "scaffolding/class_names_transformer"
require "scaffolding/oauth_providers"
require "scaffolding/routes_file_manipulator"

require_relative "../bullet_train/terminal_commands"

FIELD_PARTIALS = {
  address_field: nil,
  boolean: "boolean",
  buttons: "string",
  checkbox: "boolean",
  cloudinary_image: "string",
  color_picker: "string",
  date_and_time_field: "datetime",
  date_field: "date",
  email_field: "string",
  emoji_field: "string",
  file_field: "attachment",
  image: "attachment",
  options: "string",
  password_field: "string",
  phone_field: "string",
  super_select: "string",
  text_area: "text",
  text_field: "string",
  number_field: "integer",
  trix_editor: "text",
  code_editor: "text"
}

# filter out options.
argv = []
@options = {}
ARGV.each do |arg|
  if arg[0..1] == "--"
    arg = arg[2..]
    if arg.split("=").count > 1
      @options[arg.split("=")[0]] = arg.split("=")[1]
    else
      @options[arg] = true
    end
  else
    argv << arg
  end
end

def get_untracked_files
  `git ls-files --other --exclude-standard`.split("\n")
end

# class_name is a potentially namespaced class like
# "Tasks::Widget" or "Task::Widget". Here we ensure that
# the namespace doesn't clobber an existing model. If it does
# we suggest that the namespace could be pluralized.
def check_class_name_for_namespace_conflict(class_name)
  if class_name.include?("::")
    parts = class_name.split("::") # ["Task", "Widget"]
    # We drop the last segment because that's tne new model we're trying to create
    parts.pop # ["Task"]
    possible_conflicted_class_name = ""
    parts.each do |part|
      possible_conflicted_class_name += "::#{part}"
      begin
        klass = possible_conflicted_class_name.constantize
        is_active_record_class = klass&.ancestors&.include?(ActiveRecord::Base)
        is_aactive_hash_class = klass&.ancestors&.include?(ActiveHash::Base)
        if klass && (is_active_record_class || is_aactive_hash_class)
          problematic_namespace = possible_conflicted_class_name[2..]
          puts "It looks like the namespace you gave for this model conflicts with an existing class: #{klass.name}".red
          puts "You should use a namespace that doesn't clobber an existing class.".red
          puts ""
          puts "We reccomend using the pluralized version of the existing class.".red
          puts ""
          puts "For instance instead of #{problematic_namespace} use #{problematic_namespace.pluralize}".red
          exit
        end
      rescue NameError
        # this is good actually, it means we don't already have a class that will be clobbered
      end
    end
  end
end

def check_required_options_for_attributes(scaffolding_type, attributes, child, parent = nil)
  tableized_parent = nil

  # Ensure the parent attribute name has the proper namespacing for adding as a foreign key.
  if parent.present?
    if child.include?("::") && parent.include?("::")
      child_parts = child.split("::")
      parent_parts = parent.split("::")
      child_parts_dup = child_parts.dup
      parent_parts_dup = parent_parts.dup

      # Pop off however many spaces match.
      child_parts_dup.each.with_index do |child_part, idx|
        if child_part == parent_parts_dup[idx]
          child_parts.shift
          parent_parts.shift
        else
          tableized_parent = parent_parts.map(&:downcase).join("_")
          break
        end
      end
    end
    # In case we're not working with namespaces, just tableize the parent as is.
    tableized_parent ||= parent.tableize.singularize.tr("/", "_") if parent.present?
  end

  generation_command = case scaffolding_type
  when "crud"
    "bin/rails generate model #{child} #{tableized_parent}:references"
  when "crud-field"
    "" # This is blank so we can create the proper migration name first after we get the attributes.
  end

  # Even if there are attributes passed to the scaffolder,
  # They may already exist in previous migrations, so we
  # only register ones that need to be generated.
  # i.e. - *_ids attributes in the join-model scaffolder.
  attributes_to_generate = []

  attributes.each do |attribute|
    parts = attribute.split(":")
    name = parts.shift
    type = parts.join(":")
    type_without_option = type.gsub(/{.*}/, "")

    unless Scaffolding.valid_attribute_type?(type)
      raise "You have entered an invalid attribute type: #{type}. General data types are used when creating new models, but Bullet Train " \
        "uses field partials when Super Scaffolding, i.e. - `name:text_field` as opposed to `name:string`. " \
        "Please refer to the Field Partial documentation to view which attribute types are available."
    end

    # extract any options they passed in with the field.
    type, attribute_options = type.scan(/^(.*){(.*)}/).first || type

    # create a hash of the options.
    attribute_options = if attribute_options
      attribute_options.split(",").map do |s|
        option_name, option_value = s.split("=")
        [option_name.to_sym, option_value || true]
      end.to_h
    else
      {}
    end

    if type == "image" && cloudinary_enabled? && attribute_options[:multiple]
      puts "You have Cloudinary enabled and tried to scaffold an image field with the `multiple` option. " \
        "At this time we do not support multiple images in a single Cloudinary image attribute. " \
        "We hope to add support for it in the future. " \
        "For now you could use individual named image attributes, or you might try disabling Cloudinary and using ActiveStorage.".red
      exit
    end

    data_type = if type == "image" && cloudinary_enabled?
      "string"
    elsif attribute_options[:multiple]
      case type
      when "file_field"
        "attachments"
      when "image"
        "attachments"
      else
        "jsonb"
      end
    else
      FIELD_PARTIALS[type_without_option.to_sym]
    end

    if name.match?(/_id$/) || name.match?(/_ids$/)
      attribute_options ||= {}
      unless attribute_options[:vanilla]
        name_without_id = if name.match?(/_id$/)
          name.delete_suffix("_id")
        elsif name.match?(/_ids$/)
          name.delete_suffix("_ids")
        end

        attribute_options[:class_name] ||= name_without_id.classify

        file_name = Dir.glob("app/models/**/*.rb").find { |model| model.match?(/#{attribute_options[:class_name].underscore}\.rb/) } || ""

        begin
          class_name_constant = attribute_options[:class_name].constantize
        rescue NameError
          if attribute_options[:class_name] == child
            puts ""
            puts "You appear to be tryingo scaffold a model that references itself. Unfotunately this needs to be a two-step process.".red
            puts "First you should generate the model without the reference, and then add the reference as a new field. For instance:".red
            puts ""
            puts "  rails generate super_scaffold #{child}#{" " + parent if parent.present?}".red
            puts "  rails generate super_scaffold:field #{child} #{name}:#{type}".red
            puts ""
            puts "If `#{name}` is just a regular field and isn't backed by an ActiveRecord association, you can skip all this with the `{vanilla}` option, e.g.:".red
            puts ""
            puts "  rails generate super_scaffold #{child}#{" " + parent if parent.present?} #{name}:#{type}{vanilla}".red
            puts ""
            exit
          else
            # We don't do anything special here because we'll end up triggering the error message below. A self-referential model
            # is kind of a special case that's worth calling out specifically. If we just can't find the model the messaging below
            # should be sufficient to get folks on the right track.
          end
        end

        # If a model is namespaced, the parent's model file might exist under
        # `app/models/`, but sometimes these files are modules that resolve
        # table names by providing a prefix as opposed to an actual ApplicationRecord.
        # This check ensures that the _id attribute really is a model.
        is_active_record_class = class_name_constant&.ancestors&.include?(ActiveRecord::Base)
        unless File.exist?(file_name) && is_active_record_class
          puts ""
          puts "Attributes that end with `_id` or `_ids` trigger awesome, powerful magic in Super Scaffolding. However, because no `#{attribute_options[:class_name]}` class was found defined in your app, you'll need to specify a `class_name` that exists to let us know what model class is on the other side of the association, like so:".red
          puts ""
          puts "  bin/super-scaffold #{scaffolding_type} #{child}#{" " + parent if parent.present?} #{name}:#{type}{class_name=#{name.gsub(/_ids?$/, "").classify}}".red
          puts ""
          puts "If `#{name}` is just a regular field and isn't backed by an ActiveRecord association, you can skip all this with the `{vanilla}` option, e.g.:".red
          puts ""
          puts "  bin/super-scaffold #{scaffolding_type} #{child}#{" " + parent if parent.present?} #{name}:#{type}{vanilla}".red
          puts ""
          exit
        end
      end
    end

    # TODO: Is there ever a case that we want this to be a string?
    data_type = "references" if name.match?(/_id$/)

    # For join models, we don't want to generate a migration when
    # running the crud-field scaffolder in the last step, so we skip *_ids.
    # Addresses belong_to :addressable, so they don't have to be represented in a migration.
    unless name.match?(/_ids$/) || data_type.nil?
      generation_command += " #{name_without_id || name}:#{data_type}"
      attributes_to_generate << name
    end
  end

  # Generate the models/migrations with the attributes passed.
  if attributes_to_generate.any?
    case scaffolding_type
    # "join-model" and "oauth-provider" are not here because the
    # `rails g` command is written inline in their own respective scaffolders.
    when "crud"
      puts "Generating #{child} model with '#{generation_command}'".green
    when "crud-field"
      generation_command = "bin/rails generate migration add_#{attributes_to_generate.join("_and_")}_to_#{child.tableize.tr("/", "_")}#{generation_command}"
      puts "Adding new fields to #{child} with '#{generation_command}'".green
    end
    puts ""

    unless @options["skip-migration-generation"]
      untracked_files = get_untracked_files
      generation_thread = Thread.new { `#{generation_command}` }
      generation_thread.join # Wait for the process to finish.

      newly_untracked_files = get_untracked_files
      if (newly_untracked_files - untracked_files).size.zero?
        error_message = <<~MESSAGE
          Since you have already created the #{child} model, Super Scaffolding won't allow you to re-create it.
          You can either delete the model and try Super Scaffolding again, or add the `--skip-migration-generation`
          flag to Super Scaffold the classic Bullet Train way.
        MESSAGE
        puts ""
        puts error_message.red
        exit 1
      end
    end
  end
end

# grab the _type_ of scaffold we're doing.
scaffolding_type = argv.shift

if BulletTrain::SuperScaffolding.scaffolders.include?(scaffolding_type)
  scaffolder = BulletTrain::SuperScaffolding.scaffolders[scaffolding_type].constantize
  scaffolder.new(argv, @options).run
end
