require 'digest'


module Wiki
  class Commit
    class Conflict < StandardError
      ;
    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 = normalise_body(body)
      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

        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)

        rev
      end
    end

    def redirect! redirect_page:, message:, base_revision_id:
      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

        WikiRevision.create!(
            wiki_page:        @page,
            base_revision_id:,
            created_user:     @created_user,
            kind:             :redirect,
            redirect_page:,
            message:,
            lines_count:      0,
            tree_sha256:      nil)
      end
    end

    private

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

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

    def upsert_lines! lines, line_shas
      now = Time.current

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

      missing_rows = []
      line_shas.each_with_index do |sha, i|
        next if id_by_sha.key?(sha)

        missing_rows << { sha256:     sha,
                          body:       lines[i],
                          created_at: now,
                          updated_at: now }
      end

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

      id_by_sha
    end
  end
end
