class PostCreatePlan
  VIDEO_TAG_NAME = '動画'.freeze
  TAGME_TAG_NAME = 'タグ希望'.freeze
  NO_DEERJIKIST_TAG_NAME = 'ニジラー情報不詳'.freeze

  def initialize attributes:
    @attributes = attributes.symbolize_keys
    @existing_tags_by_name = nil
  end

  def build!
    direct_tag_specs, tag_sections = parse_direct_tag_specs
    default_tag_specs = build_default_tag_specs(direct_tag_specs)
    snapshot_tag_specs = merge_tag_specs(direct_tag_specs + default_tag_specs)
    preload_existing_tags_by_name!(snapshot_tag_specs.map { _1[:name] })
    validate_new_tag_specs!(snapshot_tag_specs)
    post_tag_specs = expand_parent_tag_specs(snapshot_tag_specs)
    video_ms = normalise_video_ms(snapshot_tag_specs)
    validate_video_sections!(video_ms, tag_sections)
    parent_post_ids = normalise_parent_post_ids
    validate_parent_post_ids!(parent_post_ids)

    {
        url: @attributes[:url],
        title: @attributes[:title].to_s,
        thumbnail_base: @attributes[:thumbnail_base].presence,
        original_created_from: @attributes[:original_created_from].presence,
        original_created_before: @attributes[:original_created_before].presence,
        tags: serialised_tags(direct_tag_specs, tag_sections),
        display_tags: display_tags(direct_tag_specs, tag_sections),
        duration: @attributes[:duration].to_s,
        video_ms: video_ms,
        parent_post_ids: parent_post_ids.join(' '),
        direct_tag_specs: direct_tag_specs,
        default_tag_specs: default_tag_specs,
        snapshot_tag_specs: snapshot_tag_specs,
        post_tag_specs: post_tag_specs,
        tag_sections: tag_sections,
        normalised_parent_post_ids: parent_post_ids }
  end

  private

  def tag_names = @attributes[:tags].to_s.split

  def parse_direct_tag_specs
    tag_sections = { }
    direct_tag_specs = []

    tag_names.each do |raw_name|
      tag_name, category, sections = parse_raw_tag_name(raw_name)
      existing_tag = existing_tags_by_name[tag_name]
      raise Tag::NicoTagNormalisationError if existing_tag&.nico?
      raise Tag::DeprecatedTagNormalisationError, [existing_tag.name] if existing_tag&.deprecated?

      direct_tag_specs << {
          name: tag_name,
          category: (category || existing_tag&.category || 'general').to_sym }
      if sections.present?
        tag_sections[tag_name] ||= []
        tag_sections[tag_name].concat(sections)
        tag_sections[tag_name] = Tag.merge_section_ranges(tag_sections[tag_name])
        tag_sections.delete(tag_name) if tag_sections[tag_name] == [[0, nil]]
      end
    end

    [merge_tag_specs(direct_tag_specs), tag_sections]
  end

  def parse_raw_tag_name raw_name
    name = raw_name.to_s
    prefix, category =
        Tag::CATEGORY_PREFIXES.find {
          name.downcase.start_with?(_1[0])
        } || ['', nil]
    name = name.sub(/\A#{ prefix }/i, '')

    sections = []
    while (match = name.match(/\A(\S*?)\[([^\[\]\s]*)-([^\[\]\s]*)\](\S*)\z/))
      name = "#{ match[1] }#{ match[4] }"
      next if match[2].empty? && match[3].empty?

      sections << Tag.normalise_section_range!(
          begin_raw: match[2],
          end_raw: match[3],
          tag_name: name)
    end
    if name.include?('[') || name.include?(']')
      raise Tag::SectionLiteralParseError.new(raw_name, raw_name)
    end

    [resolved_tag_name(name), category&.to_sym, sections]
  end

  def build_default_tag_specs direct_tag_specs
    default_tag_specs = []
    if direct_tag_specs.length < 10 && direct_tag_specs.none? { _1[:name] == TAGME_TAG_NAME }
      default_tag_specs << {
          name: TAGME_TAG_NAME,
          category: :meta }
    end
    if direct_tag_specs.none? { deerjikist_tag_spec?(_1) }
      default_tag_specs << {
          name: NO_DEERJIKIST_TAG_NAME,
          category: :meta }
    end

    default_tag_specs
  end

  def validate_new_tag_specs! specs
    Array(specs).each do |spec|
      next if existing_tags_by_name.key?(spec[:name])

      validate_new_tag_spec!(spec)
    end
  end

  def validate_new_tag_spec! spec
    tag_name = TagName.new(name: spec[:name])
    tag = Tag.new(category: spec[:category], tag_name:)
    return if tag_name.valid? && tag.valid?

    post = Post.new
    tag_name.errors[:name].each do |message|
      post.errors.add :tags, "#{ spec[:name] }: #{ message }"
    end
    tag.errors.each do |error|
      next if error.attribute == :tag_name

      post.errors.add :tags, "#{ spec[:name] }: #{ error.message }"
    end
    raise ActiveRecord::RecordInvalid, post
  end

  def expand_parent_tag_specs snapshot_tag_specs
    existing_snapshot_tags = snapshot_tag_specs.filter_map { existing_tags_by_name[_1[:name]] }
    expanded_parent_specs =
        Tag.expand_parent_tags(existing_snapshot_tags)
           .reject(&:deprecated?)
           .map { |tag|
             {
                 name: tag.name,
                 category: tag.category.to_sym } }
    merge_tag_specs(snapshot_tag_specs + expanded_parent_specs)
  end

  def merge_tag_specs specs
    specs.each_with_object({ }) do |spec, merged|
      merged[spec[:name]] =
          if merged.key?(spec[:name]) && merged[spec[:name]][:category] != :general
            merged[spec[:name]]
          else
            {
                name: spec[:name],
                category: spec[:category] }
          end
    end.values.sort_by { _1[:name] }
  end

  def existing_tags_by_name
    @existing_tags_by_name ||= begin
      names = tag_names.map { canonical_tag_name_without_sections(_1) }.uniq
      Tag.joins(:tag_name).where(tag_names: { name: names }).index_by(&:name)
    end
  end

  def preload_existing_tags_by_name! names
    wanted_names = Array(names).map { _1.to_s }.reject(&:blank?).uniq
    missing_names = wanted_names - existing_tags_by_name.keys
    return if missing_names.empty?

    existing_tags_by_name.merge!(
        Tag.joins(:tag_name)
           .where(tag_names: { name: missing_names })
           .index_by(&:name))
  end

  def canonical_tag_name_without_sections raw_name
    name, = parse_raw_tag_name(raw_name)
    name
  end

  def deerjikist_tag_spec? spec
    return true if spec[:category] == :deerjikist

    existing_tags_by_name[spec[:name]]&.deerjikist?
  end

  def normalise_parent_post_ids
    Array(@attributes[:parent_post_ids]).flat_map { _1.to_s.split }.map { |token|
      id = Integer(token, exception: false)
      raise ArgumentError, "親投稿 Id. が不正です: #{ token }" if id.nil? || id <= 0

      id
    }.uniq.sort
  end

  def validate_parent_post_ids! ids
    missing = ids - Post.where(id: ids).pluck(:id)
    raise ArgumentError, "存在しない親投稿 Id. があります: #{ missing.join(' ') }" if missing.present?
  end

  def serialised_tags direct_tag_specs, tag_sections
    direct_tag_specs.map { |spec|
      "#{ spec[:name] }#{ tag_sections[spec[:name]].to_a.map { section_literal(_1) }.join }"
    }.sort.join(' ')
  end

  def display_tags direct_tag_specs, tag_sections
    direct_tag_specs.map { |spec|
      {
          name: spec[:name],
          category: spec[:category].to_s,
          section_literals: tag_sections[spec[:name]].to_a.map { section_literal(_1) } }
    }.sort_by { _1[:name] }
  end

  def section_literal range
    begin_ms, end_ms = range
    "[#{ Post.ms_to_time(begin_ms) }-#{ end_ms ? Post.ms_to_time(end_ms) : '' }]"
  end

  def normalise_video_ms snapshot_tag_specs
    return nil unless snapshot_tag_specs.any? { _1[:name] == VIDEO_TAG_NAME }

    video_ms = @attributes[:video_ms]
    if video_ms.present?
      value = Integer(video_ms, exception: false)
      raise PostCreator::VideoMsParseError unless value&.positive?

      return value
    end

    duration = @attributes[:duration]
    return nil if duration.blank?

    value = Tag.time_to_ms!(duration.to_s, tag_name: '動画時間')
    raise PostCreator::VideoMsParseError unless value.positive?

    value
  rescue Tag::SectionLiteralParseError
    raise PostCreator::VideoMsParseError
  end

  def validate_video_sections! video_ms, tag_sections
    return unless video_ms

    tag_sections.each_value do |ranges|
      ranges.each do |begin_ms, end_ms|
        if begin_ms >= video_ms
          post = Post.new
          post.errors.add :video_ms, 'タグ区間の開始が動画時間以上です．'
          raise ActiveRecord::RecordInvalid, post
        end
        if end_ms && end_ms > video_ms
          post = Post.new
          post.errors.add :video_ms, 'タグ区間の終端が動画時間を超えてゐます．'
          raise ActiveRecord::RecordInvalid, post
        end
      end
    end
  end

  def resolved_tag_name name
    tag_name = TagName.includes(:canonical).find_by(name:)
    return name if tag_name.nil?

    (tag_name.canonical || tag_name).name
  end
end
