# frozen_string_literal: true

module DiscourseNarrativeBot
  class Base
    include Actions

    class InvalidTransitionError < StandardError; end

    def input(input, user, post: nil, topic_id: nil, skip: false)
      new_post = nil
      @post = post
      @topic_id = topic_id
      @skip = skip

      synchronize(user) do
        @user = user
        @data = get_data(user) || {}
        @state = (@data[:state] && @data[:state].to_sym) || :begin
        @input = input
        opts = {}

        begin
          opts = transition

          loop do
            next_state = opts[:next_state]

            break if next_state == :end

            next_opts = self.class::TRANSITION_TABLE.fetch(next_state)
            prerequisite = next_opts[:prerequisite]

            if (!prerequisite || instance_eval(&prerequisite)) && !(
              SiteSetting.discourse_narrative_bot_skip_tutorials.present? &&
              SiteSetting.discourse_narrative_bot_skip_tutorials.split("|").include?(next_state.to_s))

              break
            end

            [:next_state, :next_instructions].each do |key|
              opts[key] = next_opts[key]
            end
          end
        rescue InvalidTransitionError
          # For given input, no transition for current state
          return
        end

        next_state = opts[:next_state]
        action = opts[:action]

        if next_instructions = opts[:next_instructions]
          @next_instructions = next_instructions
        end

        begin
          old_data = @data.dup

          new_post =
            if (@skip && @state != :end)
              skip_tutorial(next_state)
            else
              self.send(action)
            end

          if new_post
            old_state = old_data[:state]
            state_changed = (old_state.to_s != next_state.to_s)
            clean_up_state(old_state) if state_changed

            @state = @data[:state] = next_state
            @data[:last_post_id] = new_post.id
            set_data(@user, @data)

            init_state(next_state) if state_changed

            if next_state == :end
              end_reply
              cancel_timeout_job(user)

              BadgeGranter.grant(
                Badge.find_by(name: self.class.badge_name),
                user
              )

              set_data(@user,
                topic_id: new_post.topic_id,
                state: :end,
                track: self.class.to_s
              )
            end
          end
        rescue => e
          @data = old_data
          set_data(@user, @data)
          raise e
        end
      end

      new_post
    end

    def reset_bot
      not_implemented
    end

    def set_data(user, value)
      DiscourseNarrativeBot::Store.set(user.id, value)
    end

    def get_data(user)
      DiscourseNarrativeBot::Store.get(user.id)
    end

    def notify_timeout(user)
      @data = get_data(user) || {}

      if post = Post.find_by(id: @data[:last_post_id])
        reply_to(post, I18n.t("discourse_narrative_bot.timeout.message",
          i18n_post_args(
            username: user.username,
            skip_trigger: TrackSelector.skip_trigger,
            reset_trigger: "#{TrackSelector.reset_trigger} #{self.class.reset_trigger}"
          )
        ), {}, skip_send_email: false)
      end
    end

    def certificate(type = nil)
      options = {
        user_id: @user.id,
        date: Time.zone.now.strftime('%b %d %Y'),
        format: :svg
      }
      options.merge!(type: type) if type

      src = Discourse.base_url + DiscourseNarrativeBot::Engine.routes.url_helpers.certificate_path(options)
      alt = CGI.escapeHTML(I18n.t("#{self.class::I18N_KEY}.certificate.alt"))

      "<iframe class='discobot-certificate' src='#{src}' width='650' height='464' alt='#{alt}'></iframe>"
    end

    protected

    def set_state_data(key, value)
      @data[@state] ||= {}
      @data[@state][key] = value
      set_data(@user, @data)
    end

    def get_state_data(key)
      @data[@state] ||= {}
      @data[@state][key]
    end

    def reset_data(user, additional_data = {})
      old_data = get_data(user)
      new_data = additional_data
      set_data(user, new_data)
      new_data
    end

    def transition
      options = self.class::TRANSITION_TABLE.fetch(@state).dup
      input_options = options.fetch(@input)
      options.merge!(input_options) unless @skip
      options
    rescue KeyError
      raise InvalidTransitionError.new
    end

    def skip_tutorial(next_state)
      return unless valid_topic?(@post.topic_id)

      fake_delay

      if next_state != :end
        reply = reply_to(@post, instance_eval(&@next_instructions))
        enqueue_timeout_job(@user)
        reply
      else
        @post
      end
    end

    def i18n_post_args(extra = {})
      { base_uri: Discourse.base_path }.merge(extra)
    end

    def valid_topic?(topic_id)
      topic_id == @data[:topic_id]
    end

    def not_implemented
      raise 'Not implemented.'
    end

    private

    def clean_up_state(state)
      clean_up_method = "clean_up_#{state}"
      self.send(clean_up_method) if self.class.private_method_defined?(clean_up_method)
    end

    def init_state(state)
      init_method = "init_#{state}"
      self.send(init_method) if self.class.private_method_defined?(init_method)
    end
  end
end