require 'net/http'
require 'uri'
require 'set'


class TagsController < ApplicationController
  def index
    post_id = params[:post]

    name = params[:name].presence
    category = params[:category].presence
    post_count_between = (params[:post_count_gte].presence || -1).to_i,
                         (params[:post_count_lte].presence || -1).to_i
    post_count_between[0] = nil if post_count_between[0] < 0
    post_count_between[1] = nil if post_count_between[1] < 0
    created_between = params[:created_from].presence, params[:created_to].presence
    updated_between = params[:updated_from].presence, params[:updated_to].presence
    deprecated_given = params.key?(:deprecated)
    deprecated = bool?(:deprecated)

    order = params[:order].to_s.split(':', 2).map(&:strip)
    unless order[0].in?(['name', 'category', 'post_count', 'created_at', 'updated_at'])
      order[0] = 'post_count'
    end
    unless order[1].in?(['asc', 'desc'])
      order[1] = order[0].in?(['name', 'category']) ? 'asc' : 'desc'
    end

    page = (params[:page].presence || 1).to_i
    limit = (params[:limit].presence || 20).to_i

    page = 1 if page < 1
    limit = 1 if limit < 1

    offset = (page - 1) * limit

    q =
        if post_id.present?
          Tag.joins(:posts, :tag_name)
        else
          Tag.joins(:tag_name)
        end
        .includes(:tag_name, :materials, tag_name: :wiki_page)
    q = q.where(posts: { id: post_id }) if post_id.present?

    q = q.where('tag_names.name LIKE ?', "%#{ name }%") if name
    q = q.where(category:) if category
    q = q.where('tags.post_count >= ?', post_count_between[0]) if post_count_between[0]
    q = q.where('tags.post_count <= ?', post_count_between[1]) if post_count_between[1]
    q = q.where('tags.created_at >= ?', created_between[0]) if created_between[0]
    q = q.where('tags.created_at <= ?', created_between[1]) if created_between[1]
    q = q.where('tags.updated_at >= ?', updated_between[0]) if updated_between[0]
    q = q.where('tags.updated_at <= ?', updated_between[1]) if updated_between[1]
    if deprecated_given
      q = deprecated ? q.where.not(deprecated_at: nil) : q.where(deprecated_at: nil)
    end

    sort_sql =
        case order[0]
        when 'name'
          'tag_names.name'
        when 'category'
          'CASE tags.category ' +
          "WHEN 'deerjikist' THEN 0 " +
          "WHEN 'meme' THEN 1 " +
          "WHEN 'character' THEN 2 " +
          "WHEN 'general' THEN 3 " +
          "WHEN 'material' THEN 4 " +
          "WHEN 'meta' THEN 5 " +
          "WHEN 'nico' THEN 6 END"
        else
          "tags.#{ order[0] }"
        end
    tags = q.order(Arel.sql("#{ sort_sql } #{ order[1] }, tags.id #{ order[1] }"))
            .limit(limit)
            .offset(offset)
            .to_a

    render json: { tags: TagRepr.many(tags), count: q.size }
  end

  def with_depth
    parent_tag_id = params[:parent].to_i
    parent_tag_id = nil if parent_tag_id <= 0
    material_filter = material_filter_param(default: 'any')

    graph = build_with_depth_graph(material_filter)

    tag_ids =
        if parent_tag_id
          visible_child_tag_ids(parent_tag_id, graph)
        else
          visible_root_tag_ids(graph)
        end

    render json: tag_ids
                 .sort_by { |tag_id| graph[:tags_by_id][tag_id][:name] }
                 .map { |tag_id| with_depth_lightweight_repr(tag_id, graph) }
  end

  def autocomplete
    q = params[:q].to_s.strip.sub(/\Anot:/i, '')

    with_nico = bool?(:nico, default: true)
    present_only = bool?(:present, default: true)

    alias_rows =
        TagName
        .where('name LIKE ?', "#{ q }%")
        .where.not(canonical_id: nil)
        .pluck(:canonical_id, :name)

    matched_alias_by_tag_name_id = { }
    canonical_ids = []

    alias_rows.each do |canonical_id, alias_name|
      canonical_ids << canonical_id
      matched_alias_by_tag_name_id[canonical_id] ||= alias_name
    end

    base = Tag.joins(:tag_name)
              .includes(:tag_name, :materials, tag_name: :wiki_page)
              .where(deprecated_at: nil)
    base = base.where('tags.post_count > 0') if present_only

    canonical_hit =
        base
        .where(((with_nico ? '(tags.category = ? AND tag_names.name LIKE ?) OR ' : '') +
                'tag_names.name LIKE ?'),
               *(with_nico ? ['nico', "nico:#{ q }%"] : []), "#{ q }%")

    tags =
        if canonical_ids.present?
          canonical_hit.or(base.where(tag_name_id: canonical_ids.uniq))
        else
          canonical_hit
        end

    tags = tags.order(Arel.sql('post_count DESC, tag_names.name')).limit(20).to_a

    render json: tags.map { |tag|
      TagRepr.base(tag).merge(matched_alias: matched_alias_by_tag_name_id[tag.tag_name_id])
    }
  end

  def show
    tag = Tag.joins(:tag_name)
             .includes(:tag_name, :materials, tag_name: :wiki_page)
             .find_by(id: params[:id])
    if tag
      render json: TagRepr.base(tag)
    else
      head :not_found
    end
  end

  def show_by_name
    name = params[:name].to_s.strip
    return render_bad_request('name は必須です．') if name.blank?

    tag = Tag.joins(:tag_name)
             .includes(:tag_name, :materials, tag_name: :wiki_page)
             .find_by(tag_names: { name: })
    if tag
      render json: TagRepr.base(tag)
    else
      head :not_found
    end
  end

  def deerjikists
    tag = Tag.joins(:tag_name)
             .includes(:tag_name, tag_name: :wiki_page)
             .find_by(id: params[:id])
    return head :not_found unless tag

    render json: { tag: TagRepr.base(tag),
                   deerjikists: DeerjikistRepr.many(tag.deerjikists) }
  end

  def deerjikists_by_name
    name = params[:name].to_s.strip
    return render_bad_request('name は必須です．') if name.blank?

    tag = Tag.joins(:tag_name)
             .includes(:tag_name, tag_name: :wiki_page)
             .find_by(tag_names: { name: })
    return head :not_found unless tag

    render json: { tag: TagRepr.base(tag),
                   deerjikists: DeerjikistRepr.many(tag.deerjikists) }
  end

  def update_deerjikists
    return head :unauthorized unless current_user
    return head :forbidden unless current_user.gte_member?

    tag = Tag.joins(:tag_name)
             .includes(:tag_name, tag_name: :wiki_page)
             .find_by(id: params[:id])
    return head :not_found unless tag

    ApplicationRecord.transaction do
      tag.deerjikists = []
      params[:_json].each.with_index do |item, i|
        platform = item[:platform]
        code = normalise_deerjikist_code(platform, item[:code])
        deerjikist = Deerjikist.find_or_initialize_by(platform:, code:)
        deerjikist.tag = tag
        render_deerjikist_form_record_invalid(deerjikist, i) unless deerjikist.save
        raise ActiveRecord::Rollback if performed?
      end
    end

    return if performed?

    render json: DeerjikistRepr.many(tag.reload.deerjikists)
  end

  def materials_by_name
    name = params[:name].to_s.strip
    return render_bad_request('name は必須です．') if name.blank?
    material_filter = material_filter_param(default: 'any')

    tag = Tag.joins(:tag_name)
             .includes(:tag_name, :materials, tag_name: :wiki_page)
             .find_by(tag_names: { name: })
    return head :not_found unless tag

    graph = build_with_depth_graph(material_filter)

    render json: build_tag_children(tag, graph:)
  end

  def update_all
    return head :unauthorized unless current_user
    return head :forbidden unless current_user.gte_member?

    tag = Tag.find_by(id: params[:id])
    return head :not_found unless tag

    name = params[:name].to_s.strip
    category = params[:category].to_s.strip
    return render_unprocessable_entity('名前は必須です．', field: :name) if name.blank?
    return render_unprocessable_entity('カテゴリは必須です．', field: :category) if category.blank?
    return render_unprocessable_entity '廃止状態は必須です．', field: :deprecated unless params.key?(:deprecated)

    return unless validate_tag_rename(tag, name)

    alias_names = params[:aliases].to_s.split.uniq
    parent_names = params[:parent_tags].to_s.split.uniq
    deprecated = bool?(:deprecated)

    if tag.nico? && deprecated
      return render_unprocessable_entity 'ニコタグは廃止できません．', field: :deprecated
    end

    if tag.nico? || category == 'nico'
      return render_unprocessable_entity 'ニコタグは変更できません．', field: :category
    end

    ApplicationRecord.transaction do
      TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)

      old_name = tag.name
      name_changed = name != old_name
      wiki_page = tag.tag_name.wiki_page if name_changed

      if tag.deprecated? == deprecated
        tag.update!(category:)
      else
        tag.update!(category:, deprecated_at: deprecated ? Time.current : nil)
      end
      rename_tag_name!(tag, name) if name_changed

      alias_names << old_name if name_changed
      alias_names.delete(name)

      update_aliases!(tag, alias_names)
      update_parent_tags!(tag, parent_names)

      tag.reload

      record_tag_version!(
          tag,
          event_type: :update,
          created_by_user: current_user,
          name_changed:,
          wiki_page:)
    end

    render json: TagRepr.base(tag.reload)
  end

  def update
    return head :unauthorized unless current_user
    return head :forbidden unless current_user.gte_member?

    name = params[:name].presence
    category = params[:category].presence
    deprecated_given = params.key?(:deprecated)
    deprecated = bool?(:deprecated)

    tag = Tag.find(params[:id])

    if tag.nico? && deprecated_given && deprecated
      return render_unprocessable_entity 'ニコタグは廃止できません．', field: :deprecated
    end

    return unless validate_tag_rename(tag, name)

    if tag.nico? || (category.present? && category == 'nico')
      return render_unprocessable_entity 'ニコタグは変更できません．', field: :category
    end

    ApplicationRecord.transaction do
      TagVersioning.ensure_snapshot!(tag, created_by_user: current_user)

      old_name = tag.name
      name_changed = name.present? && name != old_name
      wiki_page = tag.tag_name.wiki_page if name_changed

      rename_tag_name!(tag, name) if name_changed
      tag.update!(category:) if category.present?
      if deprecated_given && tag.deprecated? != deprecated
        tag.update!(deprecated_at: deprecated ? Time.current : nil)
      end

      tag.reload

      record_tag_version!(
          tag,
          event_type: :update,
          created_by_user: current_user,
          name_changed:,
          wiki_page:)
    end

    render json: TagRepr.base(tag.reload)
  end

  private

  def material_filter_param default:
    value = params[:material_filter].to_s.presence
    return default unless ['present', 'missing', 'any'].include?(value)

    value
  end

  def build_with_depth_graph material_filter
    children_by_parent_id = Hash.new { |h, k| h[k] = [] }
    parent_ids_by_child_id = Hash.new { |h, k| h[k] = [] }

    TagImplication.pluck(:parent_tag_id, :tag_id).each do |parent_id, child_id|
      children_by_parent_id[parent_id] << child_id
      parent_ids_by_child_id[child_id] << parent_id
    end

    tag_ids = (children_by_parent_id.keys +
               parent_ids_by_child_id.keys +
               Tag.where(category: ['meme', 'character', 'material']).pluck(:id)).uniq

    material_tag_ids = Material.unscoped.kept.where.not(tag_id: nil).distinct.pluck(:tag_id).to_set

    tags_by_id = Tag.joins(:tag_name)
                    .where(id: tag_ids)
                    .pluck('tags.id', 'tag_names.name', 'tags.category', 'tags.deprecated_at')
                    .each_with_object({ }) do |(id, name, category, deprecated_at), h|
      h[id] = { name:, category:, deprecated: deprecated_at.present?,
                has_material: material_tag_ids.include?(id) }
    end

    { children_by_parent_id:, parent_ids_by_child_id:, tags_by_id:,
      visible_child_tag_ids_by_parent_id: { },
      visible_subtree_by_tag_id: { },
      material_filter: }
  end

  def visible_root_tag_ids graph
    root_ids = Set.new

    graph[:tags_by_id].each do |tag_id, attrs|
      next unless visible_root_tag?(tag_id, graph)
      next unless visible_subtree?(tag_id, graph)

      if attrs[:deprecated]
        collect_visible_child_tag_ids(tag_id, graph, root_ids, Set.new)
      else
        root_ids << tag_id
      end
    end

    root_ids.to_a
  end

  def visible_root_tag? tag_id, graph
    seen = Set.new([tag_id])
    stack = graph[:parent_ids_by_child_id][tag_id].dup

    until stack.empty?
      parent_id = stack.pop
      next if seen.include?(parent_id)

      seen << parent_id

      parent = graph[:tags_by_id][parent_id]
      next unless parent

      return false unless parent[:deprecated]

      stack.concat(graph[:parent_ids_by_child_id][parent_id])
    end

    true
  end

  def visible_child_tag_ids parent_tag_id, graph
    cache = graph[:visible_child_tag_ids_by_parent_id]
    return cache[parent_tag_id] if cache.key?(parent_tag_id)

    visible_ids = Set.new

    graph[:children_by_parent_id][parent_tag_id].each do |child_tag_id|
      collect_visible_child_tag_ids(child_tag_id, graph, visible_ids, Set.new([parent_tag_id]))
    end

    cache[parent_tag_id] = visible_ids.to_a
  end

  def collect_visible_child_tag_ids tag_id, graph, visible_ids, seen
    return if seen.include?(tag_id)

    seen = seen.dup << tag_id
    tag = graph[:tags_by_id][tag_id]
    return unless tag

    if tag[:deprecated]
      graph[:children_by_parent_id][tag_id].each do |child_tag_id|
        collect_visible_child_tag_ids(child_tag_id, graph, visible_ids, seen)
      end
      return
    end

    if with_depth_visible_tag?(tag, graph[:material_filter])
      visible_ids << tag_id
      return
    end

    visible_ids << tag_id if graph[:children_by_parent_id][tag_id].any? { |child_tag_id|
      visible_subtree?(child_tag_id, graph)
    }
  end

  def with_depth_visible_tag? tag, material_filter
    return false unless tag[:category].in?(['meme', 'character', 'material']) && !tag[:deprecated]

    case material_filter
    when 'present'
      tag[:has_material]
    when 'missing'
      !tag[:has_material]
    else
      true
    end
  end

  def visible_subtree? tag_id, graph
    cache = graph[:visible_subtree_by_tag_id]
    return cache[tag_id] if cache.key?(tag_id)

    tag = graph[:tags_by_id][tag_id]
    return cache[tag_id] = false unless tag

    if tag[:deprecated]
      return cache[tag_id] =
                 graph[:children_by_parent_id][tag_id].any? { |child_tag_id|
                   visible_subtree?(child_tag_id, graph)
                 }
    end

    cache[tag_id] =
        with_depth_visible_tag?(tag, graph[:material_filter]) ||
        graph[:children_by_parent_id][tag_id].any? { |child_tag_id|
          visible_subtree?(child_tag_id, graph)
        }
  end

  def with_depth_lightweight_repr tag_id, graph
    tag = graph[:tags_by_id][tag_id]

    { id: tag_id,
      name: tag[:name],
      category: tag[:category],
      deprecated: tag[:deprecated],
      has_material: tag[:has_material],
      has_children: visible_child_tag_ids(tag_id, graph).present?,
      children: [] }
  end

  def build_tag_children tag, graph: nil
    material = tag.materials.first
    tag_graph = graph && graph[:tags_by_id][tag.id]
    material =
        nil if tag_graph && !with_depth_visible_tag?(tag_graph, graph[:material_filter])

    children = tag.children.sort_by(&:name)
    if graph
      children = children.filter { |child_tag| visible_subtree?(child_tag.id, graph) }
    end

    TagRepr.base(tag).merge(
        children: children.map { build_tag_children(_1, graph:) },
        has_material: tag_graph ? tag_graph[:has_material] : material.present?,
        material: material && MaterialRepr.base(material, host: request.base_url))
  end

  def record_tag_version! tag, event_type:, created_by_user:, name_changed: false, wiki_page: nil
    if tag.nico?
      NicoTagVersionRecorder.record!(tag:, event_type:, created_by_user:)
      return
    end

    TagVersionRecorder.record!(tag:, event_type:, created_by_user:)

    return unless name_changed

    wiki_page ||= tag.tag_name.wiki_page
    return unless wiki_page&.wiki_versions&.exists?

    WikiVersionRecorder.record!(
        page: wiki_page,
        event_type: :update,
        created_by_user:)
  end

  def validate_tag_rename tag, name
    return true if name.blank? || name == tag.name

    if tag.in?([Tag.tagme, Tag.bot, Tag.no_deerjikist, Tag.video, Tag.niconico])
      render_unprocessable_entity 'システム・タグの名称は変更できません．', field: :name
      return false
    end

    target_tag_name = TagName.with_discarded.find_by(name:)
    return true if target_tag_name.nil?
    return true if target_tag_name.canonical_id?

    render_unprocessable_entity 'その名前は既に使はれてゐます．', field: :name
    false
  end

  def rename_tag_name! tag, name
    return if name == tag.name

    current_tag_name = tag.tag_name
    target_tag_name = TagName.with_discarded.find_by(name:)

    if target_tag_name.nil?
      current_tag_name.update!(name:)
      return
    end

    promote_tag_alias!(
        tag,
        current_tag_name:,
        promoted_tag_name: target_tag_name)
  end

  def promote_tag_alias! tag, current_tag_name:, promoted_tag_name:
    old_owner_tag = promoted_tag_name.canonical&.tag

    if old_owner_tag && old_owner_tag != tag
      TagVersioning.ensure_snapshot!(old_owner_tag, created_by_user: current_user)
    end

    promoted_tag_name.undiscard! if promoted_tag_name.discarded?
    promoted_tag_name.update!(canonical: nil)

    TagName.with_discarded
           .where(canonical_id: current_tag_name.id)
           .where.not(id: promoted_tag_name.id)
           .find_each do |alias_tag_name|
      alias_tag_name.update!(canonical: promoted_tag_name)
    end

    current_tag_name.wiki_page&.update!(tag_name: promoted_tag_name)
    tag.update!(tag_name: promoted_tag_name)
    current_tag_name.association(:wiki_page).reset
    current_tag_name.association(:tag).reset
    current_tag_name.reload.update!(canonical: promoted_tag_name)

    return unless old_owner_tag && old_owner_tag != tag

    record_tag_version!(old_owner_tag.reload, event_type: :update, created_by_user: current_user)
  end

  def update_aliases! tag, alias_names
    alias_names = alias_names.uniq

    affected_tags = [tag]

    current_aliases = tag.tag_name.aliases.to_a

    current_aliases.each do |alias_tag_name|
      next if alias_names.include?(alias_tag_name.name)

      affected_tags << alias_tag_name.canonical&.tag
    end

    alias_names.each do |alias_name|
      alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
      affected_tags << alias_tag_name.canonical&.tag
    end

    affected_tags.compact.uniq.each do |affected_tag|
      TagVersioning.ensure_snapshot!(affected_tag, created_by_user: current_user)
    end

    current_aliases.each do |alias_tag_name|
      next if alias_names.include?(alias_tag_name.name)

      alias_tag_name.update!(canonical: nil)
    end

    alias_names.each do |alias_name|
      alias_tag_name = TagName.find_undiscard_or_create_by!(name: alias_name)
      alias_tag_name.update!(canonical: tag.tag_name)
    end

    affected_tags.compact.uniq.each do |affected_tag|
      record_tag_version!(affected_tag, event_type: :update, created_by_user: current_user)
    end
  end

  def update_parent_tags! tag, parent_names
    parent_tags = Tag.normalise_tags!(parent_names, with_tagme: false,
                                                    with_no_deerjikist: false,
                                                    deny_nico: true)

    old_parent_tags = tag.parents.to_a

    TagVersioning.record_tag_snapshots!((old_parent_tags + parent_tags).uniq,
                                        created_by_user: current_user)

    tag.reversed_tag_implications.destroy_all

    parent_tags.each do |parent_tag|
      next if parent_tag == tag

      TagImplication.create!(tag:, parent_tag:)
    end
  end

  def normalise_deerjikist_code platform, code
    return code if platform != 'youtube' || code[0] != '@'

    url = "https://www.youtube.com/#{ code }"

    html = Net::HTTP.get(URI(url))

    canonical = html[
        /<link rel="canonical" href="https:\/\/www\.youtube\.com\/channel\/(UC[a-zA-Z0-9_-]{22})"/,
        1]
    return canonical if canonical

    html[/"channelId":"(UC[a-zA-Z0-9_-]{22})"/, 1] || html[/\bUC[a-zA-Z0-9_-]{22}\b/]
  rescue
    nil
  end

  def render_deerjikist_form_record_invalid deerjikist, index
    fields = { }

    deerjikist.errors.each do |error|
      field =
          case error.attribute
          when :platform, :code
            "deerjikists.#{ index }.#{ error.attribute }"
          else
            :deerjikists
          end

      fields[field] ||= []
      fields[field] << error.full_message
    end

    render_validation_error fields:
  end
end
