require 'digest'


module Wiki
  class Commit
    class Conflict < StandardError
      ;
    end

    def self.create_content! tag_name:, body:, created_by_user:, message: nil
      normalised = normalise_body(body)

      page = WikiPage.new(tag_name:,
                          body: normalised,
                          created_user: created_by_user,
                          updated_user: created_by_user)

      if normalised.blank?
        page.errors.add(:body, :blank)
        raise ActiveRecord::RecordInvalid, page
      end

      ActiveRecord::Base.transaction do
        page.save!

        new(page:, created_user: created_by_user).content!(
            body: normalised,
            message:,
            base_revision_id: nil)

        page
      end
    end

    def self.content! page:, body:, created_user:, message: nil, base_revision_id: nil
      new(page:, created_user:).content!(body:, message:, base_revision_id:)
    end

    def self.redirect! page:, redirect_page:, created_user:, message: nil, base_revision_id: nil
      new(page:, created_user:).redirect!(redirect_page:, message:, base_revision_id:)
    end

    def initialize page:, created_user:
      @page = page
      @created_user = created_user
    end

    def content! body:, message:, base_revision_id:
      normalised = self.class.normalise_body(body)
      if normalised.blank?
        @page.errors.add(:body, :blank)
        raise ActiveRecord::RecordInvalid, @page
      end

      lines = split_lines(normalised)

      line_shas = lines.map { |line| Digest::SHA256.hexdigest(line) }
      tree_sha = Digest::SHA256.hexdigest(line_shas.join(','))

      line_id_by_sha = upsert_lines!(lines, line_shas)
      line_ids = line_shas.map { |sha| line_id_by_sha.fetch(sha) }

      ActiveRecord::Base.transaction do
        @page.lock!

        if base_revision_id.present?
          current_id = @page.wiki_revisions.maximum(:id)
          if current_id && current_id != base_revision_id.to_i
            raise Conflict,
                "競合が発生してゐます" +
                "（現在の Id.：#{ current_id }，ベース Id.：#{ base_revision_id }）．"
          end
        end

        @page.update!(body: normalised)

        WikiVersionRecorder.record!(
            page: @page,
            event_type: @page.wiki_versions.exists? ? :update : :create,
            reason: message,
            created_by_user: @created_user)

        rev = WikiRevision.create!(
            wiki_page:        @page,
            base_revision_id:,
            created_user:     @created_user,
            kind:             :content,
            redirect_page_id: nil,
            message:,
            lines_count:      lines.length,
            tree_sha256:      tree_sha)

        rows = line_ids.each_with_index.map do |line_id, pos|
          { wiki_revision_id: rev.id, wiki_line_id: line_id, position: pos }
        end
        WikiRevisionLine.insert_all!(rows) if rows.any?

        rev
      end
    end

    def redirect!(redirect_page:, message:, base_revision_id:) = raise '廃止しました．'

    def self.normalise_body body
      s = body.to_s
      s.gsub!(/\r\n?/, "\n")
      s.encode('UTF-8', invalid: :replace, undef: :replace, replace: '🖕')
      s.gsub(/\n+$/, '')
    end

    private

    def split_lines(body) = body.split("\n")

    def upsert_lines! lines, line_shas
      now = Time.current

      id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h

      missing_by_sha = { }

      line_shas.each_with_index do |sha, i|
        next if id_by_sha.key?(sha)
        next if missing_by_sha.key?(sha)

        missing_by_sha[sha] = {
            sha256:     sha,
            body:       lines[i],
            created_at: now,
            updated_at: now }
      end

      if missing_by_sha.any?
        WikiLine.upsert_all(missing_by_sha.values)
        id_by_sha = WikiLine.where(sha256: line_shas).pluck(:sha256, :id).to_h
      end

      id_by_sha
    end
  end
end
