class PostsController < ApplicationController
  Event = Struct.new(:post, :tag, :user, :change_type, :timestamp, keyword_init: true)

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

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

    offset = (page - 1) * limit

    sort_sql =
        'COALESCE(posts.original_created_before - INTERVAL 1 SECOND,' +
        'posts.original_created_from,' +
        'posts.created_at)'
    q =
        filtered_posts
        .preload(tags: :tag_name)
        .with_attached_thumbnail
        .select("posts.*, #{ sort_sql } AS sort_ts")
        .order(Arel.sql("#{ sort_sql } DESC"))
    posts = (
        if cursor
          q.where("#{ sort_sql } < ?", Time.iso8601(cursor)).limit(limit + 1)
        else
          q.limit(limit).offset(offset)
        end).to_a

    next_cursor = nil
    if cursor && posts.length > limit
      next_cursor = posts.last.read_attribute('sort_ts').iso8601(6)
      posts = posts.first(limit)
    end

    render json: { posts: posts.map { |post|
      post.as_json(include: { tags: { only: [:id, :category, :post_count],
                                      methods: [:name, :has_wiki] } }).tap do |json|
        json['thumbnail'] =
            if post.thumbnail.attached?
              rails_storage_proxy_url(post.thumbnail, only_path: false)
            else
              nil
            end
      end
    }, count: if filtered_posts.group_values.present?
      filtered_posts.count.size
    else
      filtered_posts.count
    end, next_cursor: }
  end

  def random
    post = filtered_posts.preload(tags: :tag_name).order('RAND()').first
    return head :not_found unless post

    viewed = current_user&.viewed?(post) || false

    render json: (post
                  .as_json(include: { tags: { only: [:id, :category, :post_count],
                                              methods: [:name, :has_wiki] } })
                  .merge(viewed:))
  end

  def show
    post = Post.includes(tags: :tag_name).find(params[:id])
    return head :not_found unless post

    viewed = current_user&.viewed?(post) || false

    json = post.as_json
    json['tags'] = build_tag_tree_for(post.tags)
    json['related'] = post.related(limit: 20)
    json['viewed'] = viewed

    render json:
  end

  def create
    return head :unauthorized unless current_user
    return head :forbidden unless current_user.member?

    # TODO: サイトに応じて thumbnail_base 設定
    title = params[:title].presence
    url = params[:url]
    thumbnail = params[:thumbnail]
    tag_names = params[:tags].to_s.split(' ')
    original_created_from = params[:original_created_from]
    original_created_before = params[:original_created_before]

    post = Post.new(title:, url:, thumbnail_base: nil, uploaded_user: current_user,
                    original_created_from:, original_created_before:)
    post.thumbnail.attach(thumbnail)
    if post.save
      post.resized_thumbnail!
      tags = Tag.normalise_tags(tag_names)
      tags = Tag.expand_parent_tags(tags)
      sync_post_tags!(post, tags)

      post.reload
      render json: post.as_json(include: { tags: { only: [:id, :category, :post_count],
                                                   methods: [:name, :has_wiki] } }),
             status: :created
    else
      render json: { errors: post.errors.full_messages }, status: :unprocessable_entity
    end
  rescue Tag::NicoTagNormalisationError
    head :bad_request
  end

  def viewed
    return head :unauthorized unless current_user

    current_user.viewed_posts << Post.find(params[:id])
    head :no_content
  end

  def unviewed
    return head :unauthorized unless current_user

    current_user.viewed_posts.delete(Post.find(params[:id]))
    head :no_content
  end

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

    title = params[:title].presence
    tag_names = params[:tags].to_s.split(' ')
    original_created_from = params[:original_created_from]
    original_created_before = params[:original_created_before]

    post = Post.find(params[:id].to_i)
    if post.update(title:, original_created_from:, original_created_before:)
      tags = post.tags.where(category: 'nico').to_a +
             Tag.normalise_tags(tag_names, with_tagme: false)
      tags = Tag.expand_parent_tags(tags)
      sync_post_tags!(post, tags)

      post.reload
      json = post.as_json
      json['tags'] = build_tag_tree_for(post.tags)
      render json:, status: :ok
    else
      render json: post.errors, status: :unprocessable_entity
    end
  rescue Tag::NicoTagNormalisationError
    head :bad_request
  end

  def changes
    id = params[:id]
    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

    pts = PostTag.with_discarded
    pts = pts.where(post_id: id) if id.present?
    pts = pts.includes(:post, { tag: :tag_name }, :created_user, :deleted_user)

    events = []
    pts.each do |pt|
      events << Event.new(
          post:         pt.post,
          tag:          pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki]),
          user:         pt.created_user && { id: pt.created_user.id, name: pt.created_user.name },
          change_type:  'add',
          timestamp:    pt.created_at)

      if pt.discarded_at
        events << Event.new(
            post:         pt.post,
            tag:          pt.tag.as_json(only: [:id, :category], methods: [:name, :has_wiki]),
            user:         pt.deleted_user && { id: pt.deleted_user.id, name: pt.deleted_user.name },
            change_type:  'remove',
            timestamp:    pt.discarded_at)
      end
    end
    events.sort_by!(&:timestamp)
    events.reverse!

    render json: { changes: events.slice(offset, limit).as_json, count: events.size }
  end

  private

  def filtered_posts
    tag_names = params[:tags].to_s.split(' ')
    match_type = params[:match]
    if tag_names.present?
      filter_posts_by_tags(tag_names, match_type)
    else
      Post.all
    end
  end

  def filter_posts_by_tags tag_names, match_type
    tag_names = TagName.canonicalise(tag_names)

    posts = Post.joins(tags: :tag_name)

    if match_type == 'any'
      posts.where(tag_names: { name: tag_names }).distinct
    else
      posts.where(tag_names: { name: tag_names })
           .group('posts.id')
           .having('COUNT(DISTINCT tag_names.id) = ?', tag_names.uniq.size)
    end
  end

  def sync_post_tags! post, desired_tags
    desired_tags.each do |t|
      t.save! if t.new_record?
    end

    desired_ids = desired_tags.map(&:id).to_set
    current_ids = post.tags.pluck(:id).to_set

    to_add = desired_ids - current_ids
    to_remove = current_ids - desired_ids

    Tag.where(id: to_add).find_each do |tag|
      begin
        PostTag.create!(post:, tag:, created_user: current_user)
      rescue ActiveRecord::RecordNotUnique
        ;
      end
    end

    PostTag.where(post_id: post.id, tag_id: to_remove.to_a).kept.find_each do |pt|
      pt.discard_by!(current_user)
    end
  end

  def build_tag_tree_for tags
    tags = tags.to_a
    tag_ids = tags.map(&:id)

    implications = TagImplication.where(parent_tag_id: tag_ids, tag_id: tag_ids)

    children_ids_by_parent = Hash.new { |h, k| h[k] = [] }
    implications.each do |imp|
      children_ids_by_parent[imp.parent_tag_id] << imp.tag_id
    end

    child_ids = children_ids_by_parent.values.flatten.uniq

    root_ids = tag_ids - child_ids

    tags_by_id = tags.index_by(&:id)

    memo = { }

    build_node = -> tag_id, path do
      tag = tags_by_id[tag_id]
      return nil unless tag

      if path.include?(tag_id)
        return tag.as_json(only: [:id, :category, :post_count],
                           methods: [:name, :has_wiki]).merge(children: [])
      end

      if memo.key?(tag_id)
        return memo[tag_id]
      end

      new_path = path + [tag_id]
      child_ids = children_ids_by_parent[tag_id] || []

      children = child_ids.filter_map { |cid| build_node.(cid, new_path) }

      memo[tag_id] = tag.as_json(only: [:id, :category, :post_count],
                                 methods: [:name, :has_wiki]).merge(children:)
    end

    root_ids.filter_map { |id| build_node.call(id, []) }
  end
end
