# coding: utf-8
# frozen_string_literal: true

module Stealth
  class Controller

    include Stealth::Controller::Callbacks
    include Stealth::Controller::DynamicDelay
    include Stealth::Controller::Replies
    include Stealth::Controller::Messages
    include Stealth::Controller::UnrecognizedMessage
    include Stealth::Controller::CatchAll
    include Stealth::Controller::Helpers
    include Stealth::Controller::InterruptDetect
    include Stealth::Controller::DevJumps
    include Stealth::Controller::Nlp

    attr_reader :current_message, :current_service, :flow_controller,
                :action_name, :current_session_id
    attr_accessor :nlp_result, :pos

    def initialize(service_message:, pos: nil)
      @current_message = service_message
      @current_service = service_message.service
      @current_session_id = service_message.sender_id
      @nlp_result = service_message.nlp_result
      @pos = pos
      @progressed = false
    end

    def has_location?
      current_message.location.present?
    end

    def has_attachments?
      current_message.attachments.present?
    end

    def progressed?
      @progressed
    end

    def route
      raise(Stealth::Errors::ControllerRoutingNotImplemented, "Please implement `route` method in BotController")
    end

    def flow_controller
      @flow_controller ||= begin
        flow_controller = [current_session.flow_string.pluralize, 'controller'].join('_').classify.constantize
        flow_controller.new(service_message: @current_message, pos: @pos)
      end
    end

    def current_session
      @current_session ||= Stealth::Session.new(id: current_session_id)
    end

    def previous_session
      @previous_session ||= Stealth::Session.new(
        id: current_session_id,
        type: :previous
      )
    end

    def action(action: nil)
      begin
        # Grab a mutual exclusion lock on the session
        lock_session!(
          session_slug: Session.slugify(
            flow: current_session.flow_string,
            state: current_session.state_string
          )
        )

        @action_name = action
        @action_name ||= current_session.state_string

        # Check if the user needs to be redirected
        if current_session.flow.current_state.redirects_to.present?
          Stealth::Logger.l(
            topic: "redirect",
            message: "From #{current_session.session} to #{current_session.flow.current_state.redirects_to.session}"
          )
          step_to(session: current_session.flow.current_state.redirects_to, pos: @pos)
          return
        end

        run_callbacks :action do
          begin
            flow_controller.send(@action_name)
            unless flow_controller.progressed?
              run_catch_all(reason: 'Did not send replies, update session, or step')
            end
          rescue Stealth::Errors::Halted
            Stealth::Logger.l(
              topic: "session",
              message: "User #{current_session_id}: session halted."
            )
          rescue StandardError => e
            if e.class == Stealth::Errors::UnrecognizedMessage
              run_unrecognized_message(err: e)
            else
              run_catch_all(err: e)
            end
          end
        end
      ensure
        # Release mutual exclusion lock on the session
        release_lock!
      end
    end

    def step_to_in(delay, session: nil, flow: nil, state: nil, slug: nil)
      if interrupt_detected?
        run_interrupt_action
        return :interrupted
      end

      flow, state = get_flow_and_state(
        session: session,
        flow: flow,
        state: state,
        slug: slug
      )

      unless delay.is_a?(ActiveSupport::Duration)
        raise ArgumentError, "Please specify your step_to_in `delay` parameter using ActiveSupport::Duration, e.g. `1.day` or `5.hours`"
      end

      Stealth::ScheduledReplyJob.perform_in(delay, current_service, current_session_id, flow, state, current_message.target_id)
      Stealth::Logger.l(topic: "session", message: "User #{current_session_id}: scheduled session step to #{flow}->#{state} in #{delay} seconds")
    end

    def step_to_at(timestamp, session: nil, flow: nil, state: nil, slug: nil)
      if interrupt_detected?
        run_interrupt_action
        return :interrupted
      end

      flow, state = get_flow_and_state(
        session: session,
        flow: flow,
        state: state,
        slug: slug
      )

      unless timestamp.is_a?(DateTime)
        raise ArgumentError, "Please specify your step_to_at `timestamp` parameter as a DateTime"
      end

      Stealth::ScheduledReplyJob.perform_at(timestamp, current_service, current_session_id, flow, state, current_message.target_id)
      Stealth::Logger.l(topic: "session", message: "User #{current_session_id}: scheduled session step to #{flow}->#{state} at #{timestamp.iso8601}")
    end

    def step_to(session: nil, flow: nil, state: nil, slug: nil, pos: nil)
      if interrupt_detected?
        run_interrupt_action
        return :interrupted
      end

      flow, state = get_flow_and_state(
        session: session,
        flow: flow,
        state: state,
        slug: slug
      )

      step(flow: flow, state: state, pos: pos)
    end

    def update_session_to(session: nil, flow: nil, state: nil, slug: nil)
      if interrupt_detected?
        run_interrupt_action
        return :interrupted
      end

      flow, state = get_flow_and_state(
        session: session,
        flow: flow,
        state: state,
        slug: slug
      )

      update_session(flow: flow, state: state)
    end

    def set_back_to(session: nil, flow: nil, state: nil, slug: nil)
      if interrupt_detected?
        run_interrupt_action
        return :interrupted
      end

      flow, state = get_flow_and_state(
        session: session,
        flow: flow,
        state: state,
        slug: slug
      )

      store_back_to_session(flow: flow, state: state)
    end

    def step_back
      back_to_session = Stealth::Session.new(
        id: current_session_id,
        type: :back_to
      )

      if back_to_session.blank?
        raise(
          Stealth::Errors::InvalidStateTransition,
          'back_to_session not found; make sure set_back_to was called first'
        )
      end

      step_to(session: back_to_session)
    end

    def do_nothing
      @progressed = :do_nothing
    end

    def halt!
      raise Stealth::Errors::Halted
    end

    private

      def update_session(flow:, state:)
        @progressed = :updated_session
        @current_session = Session.new(id: current_session_id)

        unless current_session.flow_string == flow.to_s && current_session.state_string == state.to_s
          @current_session.set_session(new_flow: flow, new_state: state)
        end
      end

      def store_back_to_session(flow:, state:)
        back_to_session = Session.new(
          id: current_session_id,
          type: :back_to
        )
        back_to_session.set_session(new_flow: flow, new_state: state)
      end

      def step(flow:, state:, pos: nil)
        update_session(flow: flow, state: state)
        @progressed = :stepped
        @flow_controller = nil
        @current_flow = current_session.flow
        @pos = pos

        flow_controller.action(action: state)
      end

      def get_flow_and_state(session: nil, flow: nil, state: nil, slug: nil)
        if session.nil? && flow.nil? && state.nil? && slug.nil?
          raise(ArgumentError, "A session, flow, state, or slug must be specified")
        end

        if session.present?
          return session.flow_string, session.state_string
        end

        if slug.present?
          flow_state = Session.flow_and_state_from_session_slug(slug: slug)
          return flow_state[:flow], flow_state[:state]
        end

        if flow.present?
          if state.blank?
            state = FlowMap.flow_spec[flow.to_sym].states.keys.first.to_s
          end

          return flow.to_s, state.to_s
        end

        if state.present?
          return current_session.flow_string, state.to_s
        end
      end

  end
end
