class PostCreator
  class VideoMsParseError < ArgumentError; end

  attr_reader :field_warnings

  def initialize actor:, attributes:
    @actor = actor
    @attributes = attributes.symbolize_keys
    @field_warnings = { }
  end

  def create!
    thumbnail_attachment = prepare_thumbnail_attachment
    post = Post.new(title: @attributes[:title].presence,
                    url: @attributes[:url],
                    thumbnail_base: @attributes[:thumbnail_base].presence,
                    uploaded_user: @actor,
                    original_created_from: @attributes[:original_created_from].presence,
                    original_created_before: @attributes[:original_created_before].presence)

    ApplicationRecord.transaction do
      post.save!
      post.thumbnail.attach(thumbnail_attachment) if thumbnail_attachment.present?
      snapshot_tags = planned_snapshot_tags
      post_tags = planned_post_tags
      sections = planned_sections
      TagVersioning.record_tag_snapshots!(snapshot_tags, created_by_user: @actor)
      post.video_ms = planned_video_ms
      post.save!
      sync_post_tags!(post, post_tags, sections)
      sync_parent_posts!(post, planned_parent_post_ids)
      PostVersionRecorder.record!(post:, event_type: :create, created_by_user: @actor)
    end
    post
  rescue StandardError
    post&.thumbnail&.purge if post&.thumbnail&.attached?
    raise
  end

  private

  def prepare_thumbnail_attachment
    PostThumbnailAttachmentBuilder.build(
        thumbnail: @attributes[:thumbnail],
        thumbnail_base: @attributes[:thumbnail_base].presence)
  end

  def planned_snapshot_tags = planned_create_attributes[:snapshot_tags]

  def planned_post_tags = planned_create_attributes[:post_tags]

  def planned_sections = planned_create_attributes[:tag_sections]

  def planned_parent_post_ids = planned_create_attributes[:normalised_parent_post_ids]

  def planned_video_ms
    planned_create_attributes[:video_ms]
  end

  def planned_create_attributes
    @planned_create_attributes ||= begin
      if @attributes.key?(:snapshot_tag_specs)
        snapshot_tags = materialise_tags(@attributes[:snapshot_tag_specs] || [])
        post_tags = materialise_tags(@attributes[:post_tag_specs] || [])
        {
            snapshot_tags: snapshot_tags,
            post_tags: post_tags,
            tag_sections: materialise_sections(
                @attributes[:tag_sections] || { },
                snapshot_tags,
                post_tags),
            normalised_parent_post_ids: @attributes[:normalised_parent_post_ids] || [],
            video_ms: @attributes[:video_ms] }
      else
        build_materialised_plan
      end
    end
  end

  def build_materialised_plan
    plan = PostCreatePlan.new(attributes: @attributes).build!
    snapshot_tags = materialise_tags(plan[:snapshot_tag_specs] || [])
    post_tags = materialise_tags(plan[:post_tag_specs] || [])
    {
        snapshot_tags: snapshot_tags,
        post_tags: post_tags,
        tag_sections: materialise_sections(
            plan[:tag_sections] || { },
            snapshot_tags,
            post_tags),
        normalised_parent_post_ids: plan[:normalised_parent_post_ids] || [],
        video_ms: plan[:video_ms] }
  end

  def materialise_tags specs
    Array(specs).each_with_object({ }) do |spec, tags|
      name = spec[:name] || spec['name']
      category = spec[:category] || spec['category']
      next if name.blank? || category.blank?

      tag = Tag.find_or_create_by_tag_name!(name, category:)
      tag.update!(category:) if tag.category.to_sym != category.to_sym
      tags[name] ||= tag
    end.values
  end

  def materialise_sections sections_by_name, snapshot_tags, post_tags
    tags_by_name = post_tags.index_by(&:name)
    snapshot_tags.each do |tag|
      tags_by_name[tag.name] ||= tag
    end

    sections_by_name.each_with_object({ }) do |(tag_name, ranges), sections|
      tag = tags_by_name[tag_name.to_s]
      next if tag.nil?

      sections[tag.id] = Array(ranges).map { |range| [range[0], range[1]] }
    end
  end

  def sync_post_tags! post, desired_tags, sections
    desired_ids = desired_tags.map(&:id).to_set
    current_ids = post.tags.pluck(:id).to_set
    Tag.where(id: desired_ids - current_ids).find_each do |tag|
      PostTag.create_or_find_by!(post:, tag:, created_user: @actor)
    end
    PostTagSection.where(post_id: post.id).destroy_all
    sections.each do |tag_id, ranges|
      ranges.each do |begin_ms, end_ms|
        PostTagSection.create!(post_id: post.id,
                               tag_id:,
                               begin_ms:,
                               end_ms:)
      end
    end
    PostTag.where(post_id: post.id,
                  tag_id: (current_ids - desired_ids).to_a).kept.find_each do |post_tag|
      post_tag.discard_by!(@actor)
    end
  end

  def sync_parent_posts! post, ids
    if ids.include?(post.id)
      post.errors.add :parent_post_ids, '自分自身を親投稿にはできません．'
      raise ActiveRecord::RecordInvalid, post
    end
    missing = ids - Post.where(id: ids).pluck(:id)
    if missing.present?
      post.errors.add :parent_post_ids, "存在しない親投稿 Id. があります: #{ missing.join(' ') }"
      raise ActiveRecord::RecordInvalid, post
    end
    ids.each { |parent_post_id| PostImplication.create_or_find_by!(post:, parent_post_id:) }
  end
end
