class Tag < ApplicationRecord
  class NicoTagNormalisationError < ArgumentError
    ;
  end

  has_many :post_tags, inverse_of: :tag
  has_many :active_post_tags, -> { kept }, class_name: 'PostTag', inverse_of: :tag
  has_many :post_tags_with_discarded, -> { with_discarded }, class_name: 'PostTag'
  has_many :posts, through: :active_post_tags

  has_many :nico_tag_relations, foreign_key: :nico_tag_id, dependent: :destroy
  has_many :linked_tags, through: :nico_tag_relations, source: :tag

  has_many :reversed_nico_tag_relations,
           class_name: 'NicoTagRelation', foreign_key: :tag_id, dependent: :destroy
  has_many :linked_nico_tags, through: :reversed_nico_tag_relations, source: :nico_tag

  has_many :tag_implications, foreign_key: :parent_tag_id, dependent: :destroy
  has_many :children, through: :tag_implications, source: :tag

  has_many :reversed_tag_implications,
           class_name: 'TagImplication', foreign_key: :tag_id, dependent: :destroy
  has_many :parents, through: :reversed_tag_implications, source: :parent_tag

  has_many :tag_similarities, dependent: :delete_all

  belongs_to :tag_name
  delegate :name, to: :tag_name, allow_nil: true
  validates :tag_name, presence: true

  enum :category, deerjikist:  'deerjikist',
                  meme:        'meme',
                  character:   'character',
                  general:     'general',
                  material:    'material',
                  nico:        'nico',
                  meta:        'meta'

  validates :category, presence: true, inclusion: { in: Tag.categories.keys }

  validate :nico_tag_name_must_start_with_nico
  validate :tag_name_must_be_canonical

  scope :nico_tags, -> { where(category: :nico) }

  CATEGORY_PREFIXES = {
      'gen:'  => :general,
      'djk:'  => :deerjikist,
      'meme:' => :meme,
      'chr:'  => :character,
      'mtr:'  => :material,
      'meta:' => :meta }.freeze

  def name= val
    (self.tag_name ||= build_tag_name).name = val
  end

  def has_wiki
    tag_name&.wiki_page.present?
  end

  def self.tagme
    @tagme ||= find_or_create_by_tag_name!('タグ希望', category: :meta)
  end

  def self.bot
    @bot ||= find_or_create_by_tag_name!('bot操作', category: :meta)
  end

  def self.video
    @video ||= find_or_create_by_tag_name!('動画', category: :meta)
  end

  def self.niconico
    @niconico ||= find_or_create_by_tag_name!('ニコニコ', category: :meta)
  end

  def self.normalise_tags tag_names, with_tagme: true, deny_nico: true
    if deny_nico && tag_names.any? { |n| n.start_with?('nico:') }
      raise NicoTagNormalisationError
    end

    tags = tag_names.map do |name|
      pf, cat = CATEGORY_PREFIXES.find { |p, _| name.start_with?(p) } || ['', nil]
      name = TagName.canonicalise(name.delete_prefix(pf)).first
      find_or_create_by_tag_name!(name, category: (cat || :general)).tap do |tag|
        if cat && tag.category != cat
          tag.update!(category: cat)
        end
      end
    end

    tags << Tag.tagme if with_tagme && tags.size < 10 && tags.none?(Tag.tagme)
    tags.uniq(&:id)
  end

  def self.expand_parent_tags tags
    return [] if tags.blank?

    seen = Set.new
    result = []
    stack = tags.compact.dup

    until stack.empty?
      tag = stack.pop
      next unless tag

      tag.parents.each do |parent|
        next if seen.include?(parent.id)

        seen << parent.id
        result << parent
        stack << parent
      end
    end

    (result + tags).uniq { |t| t.id }
  end

  def self.find_or_create_by_tag_name! name, category:
    tn = TagName.find_or_create_by!(name: name.to_s.strip)
    tn = tn.canonical if tn.canonical_id?

    Tag.find_or_create_by!(tag_name_id: tn.id) do |t|
      t.category = category
    end
  rescue ActiveRecord::RecordNotUnique
    retry
  end

  private

  def nico_tag_name_must_start_with_nico
    n = name.to_s
    if ((category == 'nico' && !(n.start_with?('nico:'))) || 
        (category != 'nico' && n.start_with?('nico:')))
      errors.add :name, 'ニコニコ・タグの命名規則に反してゐます．'
    end
  end

  def tag_name_must_be_canonical
    if tag_name&.canonical_id?
      errors.add :tag_name, 'tag_names へは実体を示す必要があります．'
    end
  end
end
