# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class RenameSystemNamespaces < ActiveRecord::Migration
  include Gitlab::Database::MigrationHelpers
  include Gitlab::ShellAdapter
  disable_ddl_transaction!

  class User < ActiveRecord::Base
    self.table_name = 'users'
  end

  class Namespace < ActiveRecord::Base
    self.table_name = 'namespaces'
    belongs_to :parent, class_name: 'RenameSystemNamespaces::Namespace'
    has_one :route, as: :source
    has_many :children, class_name: 'RenameSystemNamespaces::Namespace', foreign_key: :parent_id
    belongs_to :owner, class_name: 'RenameSystemNamespaces::User'

    # Overridden to have the correct `source_type` for the `route` relation
    def self.name
      'Namespace'
    end

    def full_path
      if route && route.path.present?
        @full_path ||= route.path
      else
        update_route if persisted?

        build_full_path
      end
    end

    def build_full_path
      if parent && path
        parent.full_path + '/' + path
      else
        path
      end
    end

    def update_route
      prepare_route
      route.save
    end

    def prepare_route
      route || build_route(source: self)
      route.path = build_full_path
      route.name = build_full_name
      @full_path = nil
      @full_name = nil
    end

    def build_full_name
      if parent && name
        parent.human_name + ' / ' + name
      else
        name
      end
    end

    def human_name
      owner&.name
    end
  end

  class Route < ActiveRecord::Base
    self.table_name = 'routes'
    belongs_to :source, polymorphic: true
  end

  class Project < ActiveRecord::Base
    self.table_name = 'projects'

    def repository_storage_path
      Gitlab.config.repositories.storages[repository_storage]['path']
    end
  end

  DOWNTIME = false

  def up
    return unless system_namespace

    old_path = system_namespace.path
    old_full_path = system_namespace.full_path
    # Only remove the last occurrence of the path name to get the parent namespace path
    namespace_path = remove_last_occurrence(old_full_path, old_path)
    new_path = rename_path(namespace_path, old_path)
    new_full_path = join_namespace_path(namespace_path, new_path)

    Namespace.where(id: system_namespace).update_all(path: new_path) # skips callbacks & validations

    replace_statement = replace_sql(Route.arel_table[:path], old_full_path, new_full_path)
    route_matches = [old_full_path, "#{old_full_path}/%"]

    update_column_in_batches(:routes, :path, replace_statement) do |table, query|
      query.where(Route.arel_table[:path].matches_any(route_matches))
    end

    clear_cache_for_namespace(system_namespace)

    # tasks here are based on `Namespace#move_dir`
    move_repositories(system_namespace, old_full_path, new_full_path)
    move_namespace_folders(uploads_dir, old_full_path, new_full_path) if file_storage?
    move_namespace_folders(pages_dir, old_full_path, new_full_path)
  end

  def down
    # nothing to do
  end

  def remove_last_occurrence(string, pattern)
    string.reverse.sub(pattern.reverse, "").reverse
  end

  def move_namespace_folders(directory, old_relative_path, new_relative_path)
    old_path = File.join(directory, old_relative_path)
    return unless File.directory?(old_path)

    new_path = File.join(directory, new_relative_path)
    FileUtils.mv(old_path, new_path)
  end

  def move_repositories(namespace, old_full_path, new_full_path)
    repo_paths_for_namespace(namespace).each do |repository_storage_path|
      # Ensure old directory exists before moving it
      gitlab_shell.add_namespace(repository_storage_path, old_full_path)

      unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path)
        say "Exception moving path #{repository_storage_path} from #{old_full_path} to #{new_full_path}"
      end
    end
  end

  def rename_path(namespace_path, path_was)
    counter = 0
    path = "#{path_was}#{counter}"

    while route_exists?(join_namespace_path(namespace_path, path))
      counter += 1
      path = "#{path_was}#{counter}"
    end

    path
  end

  def route_exists?(full_path)
    Route.where(Route.arel_table[:path].matches(full_path)).any?
  end

  def join_namespace_path(namespace_path, path)
    if namespace_path.present?
      File.join(namespace_path, path)
    else
      path
    end
  end

  def system_namespace
    @system_namespace ||= Namespace.where(parent_id: nil)
                            .where(arel_table[:path].matches(system_namespace_path))
                            .first
  end

  def system_namespace_path
    "system"
  end

  def clear_cache_for_namespace(namespace)
    project_ids = projects_for_namespace(namespace).pluck(:id)

    update_column_in_batches(:projects, :description_html, nil) do |table, query|
      query.where(table[:id].in(project_ids))
    end

    update_column_in_batches(:issues, :description_html, nil) do |table, query|
      query.where(table[:project_id].in(project_ids))
    end

    update_column_in_batches(:merge_requests, :description_html, nil) do |table, query|
      query.where(table[:target_project_id].in(project_ids))
    end

    update_column_in_batches(:notes, :note_html, nil) do |table, query|
      query.where(table[:project_id].in(project_ids))
    end

    update_column_in_batches(:milestones, :description_html, nil) do |table, query|
      query.where(table[:project_id].in(project_ids))
    end
  end

  def projects_for_namespace(namespace)
    namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id])
    namespace_or_children = Project.arel_table[:namespace_id].in(namespace_ids)
    Project.unscoped.where(namespace_or_children)
  end

  # This won't scale to huge trees, but it should do for a handful of namespaces
  # called `system`.
  def child_ids_for_parent(namespace, ids: [])
    namespace.children.each do |child|
      ids << child.id
      child_ids_for_parent(child, ids: ids) if child.children.any?
    end
    ids
  end

  def repo_paths_for_namespace(namespace)
    projects_for_namespace(namespace).distinct
      .select(:repository_storage).map(&:repository_storage_path)
  end

  def uploads_dir
    File.join(Rails.root, "public", "uploads")
  end

  def pages_dir
    Settings.pages.path
  end

  def file_storage?
    CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
  end

  def arel_table
    Namespace.arel_table
  end
end
