#!/usr/bin/env ruby

# frozen_string_literal: true

require_relative 'sections'
require_relative 'portfolios'
require_relative 'workspaces'
require_relative 'internal/config_loader'
require_relative 'internal/task_timing'
require_relative 'internal/task_hashes'
require_relative 'internal/logging'
require_relative 'internal/thread_local'
require 'asana'

module Checkoff
  # Pull tasks from Asana
  class Tasks
    # @!parse
    #   extend CacheMethod::ClassMethods

    include Logging

    MINUTE = 60
    HOUR = MINUTE * 60
    DAY = 24 * HOUR
    REALLY_LONG_CACHE_TIME = MINUTE * 30
    LONG_CACHE_TIME = MINUTE * 15
    SHORT_CACHE_TIME = MINUTE * 5

    # @param config [Hash<Symbol, Object>]
    # @param client [Asana::Client]
    # @param workspaces [Checkoff::Workspaces]
    # @param sections [Checkoff::Sections]
    # @param portfolios [Checkoff::Portfolios]
    # @param custom_fields [Checkoff::CustomFields]
    # @param time_class [Class<Time>]
    # @param date_class [Class<Date>]
    # @param asana_task [Class<Asana::Resources::Task>]
    def initialize(config: Checkoff::Internal::ConfigLoader.load(:asana),
                   client: Checkoff::Clients.new(config: config).client,
                   workspaces: Checkoff::Workspaces.new(config: config,
                                                        client: client),
                   sections: Checkoff::Sections.new(config: config,
                                                    client: client),
                   portfolios: Checkoff::Portfolios.new(config: config,
                                                        client: client),
                   custom_fields: Checkoff::CustomFields.new(config: config,
                                                             client: client),
                   time_class: Time,
                   date_class: Date,
                   asana_task: Asana::Resources::Task)
      @config = config
      @sections = sections
      @time_class = time_class
      @date_class = date_class
      @asana_task = asana_task
      @client = client
      @portfolios = portfolios
      @custom_fields = custom_fields
      @workspaces = workspaces
      @timing = Checkoff::Timing.new(today_getter: date_class, now_getter: time_class)
    end

    # Indicates a task is ready for a person to work on it.  This is
    # subtly different than what is used by Asana to mark a date as
    # red/green!  A task is ready if it is not dependent on an
    # incomplete task and one of these is true:
    #
    # * start is null and due on is today
    # * start is null and due at is before now
    # * start on is today
    # * start at is before now
    #
    # @param task [Asana::Resources::Task]
    # @param period [Symbol<:now_or_before,:this_week>]
    # @param ignore_dependencies [Boolean]
    def task_ready?(task, period: :now_or_before, ignore_dependencies: false)
      return false if !ignore_dependencies && incomplete_dependencies?(task)

      in_period?(task, :ready, period)
    end

    # @param task [Asana::Resources::Task]
    # @param field_name [Symbol,Array]
    # @param period [Symbol<:now_or_before,:this_week>,Array] See Checkoff::Timing#in_period?_
    def in_period?(task, field_name, period)
      # @type [Date,Time,nil]
      task_date_or_time = task_timing.date_or_time_field_by_name(task, field_name)

      timing.in_period?(task_date_or_time, period)
    end

    # @param task [Asana::Resources::Task]
    # @param field_name [Symbol,Array]
    #         :start - start_at or start_on (first set)
    #         :due - due_at or due_on (first set)
    #         :ready - start_at or start_on or due_at or due_on (first set)
    #         :modified - modified_at
    #         [:custom_field, "foo"] - 'Date' custom field type named 'foo'
    #
    # @return [Date, Time, nil]
    def date_or_time_field_by_name(task, field_name)
      task_timing.date_or_time_field_by_name(task, field_name)
    end

    # Pull a specific task by name
    #
    # @param workspace_name [String]
    # @param project_name [String, Symbol]
    # @param section_name [String, nil, Symbol]
    # @param task_name [String]
    # @param only_uncompleted [Boolean]
    # @param extra_fields [Array<String>]
    # @sg-ignore
    # @return [Asana::Resources::Task, nil]
    def task(workspace_name, project_name, task_name,
             section_name: :unspecified,
             only_uncompleted: true,
             extra_fields: [])
      thread_local = Checkoff::Internal::ThreadLocal.new
      task_gid = thread_local.with_thread_local_variable(:suppress_asana_webhook_watch_creation,
                                                         true) do
        gid_for_task(workspace_name, project_name, section_name, task_name)
      end
      return nil if task_gid.nil?

      task_by_gid(task_gid, only_uncompleted: only_uncompleted, extra_fields: extra_fields)
    end
    cache_method :task, LONG_CACHE_TIME

    # @param workspace_name [String]
    # @param project_name [String, Symbol]
    # @param section_name [String, nil, Symbol]
    # @param task_name [String]
    #
    # @return [String, nil]
    # @sg-ignore
    def gid_for_task(workspace_name, project_name, section_name, task_name)
      # @sg-ignore
      t = tasks(workspace_name,
                project_name,
                section_name: section_name,
                only_uncompleted: false)
      task_for_gid = t.find { |task| task.name == task_name }
      task_for_gid&.gid
    end
    cache_method :gid_for_task, REALLY_LONG_CACHE_TIME

    # Pull a specific task by GID
    #
    # @param task_gid [String]
    # @param extra_fields [Array<String>]
    # @param only_uncompleted [Boolean]
    #
    # @return [Asana::Resources::Task, nil]
    def task_by_gid(task_gid,
                    extra_fields: [],
                    only_uncompleted: true)
      # @type [Hash]
      options = projects.task_options.fetch(:options, {})
      options[:fields] += extra_fields
      options[:completed_since] = '9999-12-01' if only_uncompleted
      client.tasks.find_by_id(task_gid, options: options)
    rescue Asana::Errors::NotFound => e
      debug e
      nil
    end
    cache_method :task_by_gid, SHORT_CACHE_TIME

    # Add a task
    #
    # @param name [String]
    # @param workspace_gid [String]
    # @param assignee_gid [String]
    #
    # @return [Asana::Resources::Task]
    def add_task(name,
                 workspace_gid: @workspaces.default_workspace_gid,
                 assignee_gid: default_assignee_gid)
      @asana_task.create(client,
                         assignee: assignee_gid,
                         workspace: workspace_gid, name: name)
    end

    # Return user-accessible URL for a task
    #
    # @param task [Asana::Resources::Task]
    #
    # @return [String] end-user URL to the task in question
    def url_of_task(task)
      "https://app.asana.com/0/0/#{task.gid}/f"
    end

    # True if any of the task's dependencies are marked incomplete
    #
    # Include 'dependencies.gid' in extra_fields of task passed in.
    #
    # @param task [Asana::Resources::Task]
    def incomplete_dependencies?(task)
      # Avoid a redundant fetch.  Unfortunately, Ruby SDK allows
      # dependencies to be fetched along with other attributes--but
      # then doesn't use it and does another HTTP GET!  At least this
      # way we can skip the extra HTTP GET in the common case when
      # there are no dependencies.
      #
      # https://github.com/Asana/ruby-asana/issues/125

      # @sg-ignore
      # @type [Enumerable<Asana::Resources::Task>, nil]
      dependencies = task.instance_variable_get(:@dependencies) || []

      dependencies.any? do |parent_task_info|
        # the real bummer though is that asana doesn't let you fetch
        # the completion status of dependencies, so we need to do this
        # regardless:
        parent_task_gid = parent_task_info.fetch('gid')

        parent_task = task_by_gid(parent_task_gid, only_uncompleted: false)
        parent_task.completed_at.nil?
      end
    end

    # @param task [Asana::Resources::Task]
    # @param extra_task_fields [Array<String>]
    #
    # @return [Array<Hash>]
    def all_dependent_tasks(task, extra_task_fields: [])
      dependent_tasks = []
      # See note above - same applies as does in @dependencies
      #
      # @type [Array<Hash>]
      dependents = task.instance_variable_get(:@dependents) || []
      dependents.each do |dependent_task_hash_or_obj|
        # seems like if we ever .inspect the task, it stashes the task
        # object instead of the hash.  Try to avoid this - but maybe we
        # need to convert if it does happen.
        raise 'Found dependent task object!' if dependent_task_hash_or_obj.is_a?(Asana::Resources::Task)

        dependent_task_hash = dependent_task_hash_or_obj

        dependent_task = task_by_gid(dependent_task_hash.fetch('gid'),
                                     only_uncompleted: true,
                                     extra_fields: ['dependents'] + extra_task_fields)
        debug { "#{task.name} has dependent task #{dependent_task.name}" }
        unless dependent_task.nil?
          dependent_tasks << dependent_task
          dependent_tasks += all_dependent_tasks(dependent_task)
        end
      end
      dependent_tasks
    end

    # Builds on the standard API representation of an Asana task with some
    # convenience keys:
    #
    # <regular keys from API response>
    # +
    # unwrapped:
    #  membership_by_section_gid: Hash<String, Hash (membership)>
    #  membership_by_project_gid: Hash<String, Hash (membership)>
    #  membership_by_project_name: Hash<String, Hash (membership)>
    # task: String (name)
    #
    # @param task [Asana::Resources::Task]
    #
    # @return [Hash]
    def task_to_h(task)
      task_hashes.task_to_h(task)
    end

    # @param task_data [Hash]
    #
    # @return [Asana::Resources::Task]
    def h_to_task(task_data)
      task_hashes.h_to_task(task_data, client: client)
    end

    # True if the task is in a project which is in the given portfolio
    #
    # @param task [Asana::Resources::Task]
    # @param portfolio_name [String]
    # @param workspace_name [String]
    def in_portfolio_named?(task,
                            portfolio_name,
                            workspace_name: @workspaces.default_workspace.name)
      portfolio_projects = @portfolios.projects_in_portfolio(workspace_name, portfolio_name)
      task.memberships.any? do |membership|
        project_gid = membership.fetch('project').fetch('gid')
        portfolio_projects.any? do |portfolio_project|
          portfolio_project.gid == project_gid
        end
      end
    end

    # @return [Hash]
    def as_cache_key
      {}
    end

    private

    # @return [Checkoff::Internal::TaskTiming]
    def task_timing
      @task_timing ||= Checkoff::Internal::TaskTiming.new(time_class: @time_class, date_class: @date_class,
                                                          client: client,
                                                          custom_fields: custom_fields)
    end

    # @return [Checkoff::Internal::TaskHashes]
    def task_hashes
      @task_hashes ||= Checkoff::Internal::TaskHashes.new
    end

    # @param workspace_name [String]
    # @param project_name [String, Symbol]
    # @param section_name [String, nil, Symbol<:unspecified>]
    # @param only_uncompleted [Boolean]
    # @param extra_fields [Array<String>]
    #
    # @return [Enumerable<Asana::Resources::Task>]
    def tasks(workspace_name, project_name,
              only_uncompleted:, extra_fields: [], section_name: :unspecified)
      if section_name == :unspecified
        project = projects.project_or_raise(workspace_name, project_name)
        projects.tasks_from_project(project,
                                    only_uncompleted: only_uncompleted,
                                    extra_fields: extra_fields)
      else
        @sections.tasks(workspace_name, project_name, section_name,
                        only_uncompleted: only_uncompleted,
                        extra_fields: extra_fields)
      end
    end
    cache_method :tasks, SHORT_CACHE_TIME

    # @return [Asana::Client]
    attr_reader :client

    # @return [Checkoff::Timing]
    attr_reader :timing

    # @return [Checkoff::CustomFields]
    attr_reader :custom_fields

    # @return [Checkoff::Projects]
    def projects
      @projects ||= @sections.projects
    end

    # @sg-ignore
    # @return [String]
    def default_assignee_gid
      @config.fetch(:default_assignee_gid)
    end
  end
end