require 'curb'
require 'sinatra/base'
require 'sinatra/partial'
require 'rack-flash'
require 'json'
require 'jira'

require_relative 'metadata'


module OpsAsk
  class App < Sinatra::Base
    set :root, OpsAsk::ROOT

    # Add flash support
    enable :sessions
    use Rack::Flash

    # Add partials support
    register Sinatra::Partial
    set :partial_template_engine, :erb
    enable :partial_underscores

    # Serve up our form
    get '/' do
      erb :index, locals: {
        jiras_for_today: issues_for(today),
        jiras_for_tomorrow: issues_for(tomorrow),
        untracked_jiras: untracked_issues,
        stragglers: straggling_issues
      }
    end

    get '/glance' do
      erb :glance, locals: {
        jiras_for_today: issues_for(today),
        jiras_for_tomorrow: issues_for(tomorrow),
        untracked_jiras: untracked_issues,
        stragglers: straggling_issues
      }
    end

    get '/untracked' do
      erb :untracked, locals: {
        untracked_jiras: untracked_issues,
        stragglers: straggling_issues
      }
    end

    get '/stragglers' do
      erb :stragglers, locals: {
        untracked_jiras: untracked_issues,
        stragglers: straggling_issues
      }
    end

    get '/sprint/:sprint_num' do
      num = params[:sprint_num]
      sprint = get_sprint(num)
      id = sprint['id']
      erb :stats, locals: {
        sprint: sprint,
        ask_stats: stats_for(
          asks_in_sprint(num),
          "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape "labels in (Sprint#{num}) AND resolution is not EMPTY"}",
          "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape "labels in (Sprint#{num}) AND resolution is EMPTY"}"
        ),
        sprint_stats: stats_for(
          items_in_sprint(num),
          "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape "sprint = #{id} AND resolution is not EMPTY"}",
          "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape "sprint = #{id} AND resolution is EMPTY"}"
        ),
        untracked_jiras: untracked_issues,
        stragglers: straggling_issues
      }
    end

    get '/sprint' do
      num = current_sprint_num
      id = current_sprint_id
      erb :stats, locals: {
        sprint: current_sprint,
        ask_stats: stats_for(
          asks_in_current_sprint,
          "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape "labels in (Sprint#{num}) AND resolution is not EMPTY"}",
          "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape "labels in (Sprint#{num}) AND resolution is EMPTY"}"
        ),
        sprint_stats: stats_for(
          items_in_current_sprint,
          "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape "sprint = #{id} AND resolution is not EMPTY"}",
          "#{settings.config[:jira_url]}/issues/?jql=#{URI::escape "sprint = #{id} AND resolution is EMPTY"}"
        ),
        untracked_jiras: untracked_issues,
        stragglers: straggling_issues
      }
    end

    # I think everyone should do this
    get '/version' do
      content_type :txt
      "opsask #{settings.config[:app_version]}"
    end

    # Try to create a JIRA
    post '/' do
      duedate = validate_room_for_new_jiras
      component, summary, description, assign_to_me, epic, ops_only = validate_jira_params
      jira = create_jira duedate, component, summary, description, assign_to_me, epic, ops_only
      if jira.nil? or !jira.has_key?('key')
        flash[:error] = [ %Q| Failure!
          JIRA had an issue processing your request. Try again?
        | ]
      else
        flash[:notice] = [ %Q| Success!
          <a href="#{settings.config[:jira_url]}/browse/#{jira['key']}">#{jira['key']}</a>
          has been created on your behalf.
        | ]
      end
      redirect '/'
    end

    # Public assets
    %w[ css img js fonts ].each do |asset|
      get "/#{asset}/:file" do
        send_file "public/#{asset}/#{params[:file]}", :disposition => 'inline'
      end
    end

    get '/favicon.ico' do
      send_file 'public/favicon.ico', :disposition => 'inline'
    end



    # This section gets called before every request. Here, we set up the
    # OAuth consumer details including the consumer key, private key,
    # site uri, and the request token, access token, and authorize paths
    before do
      options = {
        :site               => settings.config[:jira_url],
        :context_path       => '',
        :signature_method   => 'RSA-SHA1',
        :request_token_path => "#{settings.config[:jira_url]}/plugins/servlet/oauth/request-token",
        :authorize_url      => "#{settings.config[:jira_url]}/plugins/servlet/oauth/authorize",
        :access_token_path  => "#{settings.config[:jira_url]}/plugins/servlet/oauth/access-token",
        :private_key_file   => settings.config[:jira_private_key],
        :rest_base_path     => "#{settings.config[:jira_url]}/rest/api/latest",
        :consumer_key       => settings.config[:jira_consumer_key]
      }

      @jira_client = JIRA::Client.new(options)
      # @jira_client.consumer.http.set_debug_output($stderr)

      # Add AccessToken if authorised previously.
      if session[:jira_auth]
        @jira_client.set_access_token(
          session[:jira_auth][:access_token],
          session[:jira_auth][:access_key]
        )

        if @project.nil?
          @project = @jira_client.Project.find(settings.config[:project_key])
        end
      end

      # Keep a pointer to myself
      begin
        response = @jira_client.get(
          @jira_client.options[:rest_base_path] + '/myself?expand=groups'
        )
        @myself = JSON::parse response.body
        @me     = @myself['name']
      rescue JIRA::OauthClient::UninitializedAccessTokenError
      end
    end

    # Retrieves the @access_token then stores it inside a session cookie. In a real app, 
    # you'll want to persist the token in a datastore associated with the user.
    get '/callback/' do
      request_token = @jira_client.set_request_token(
        session[:request_token], session[:request_secret]
      )
      access_token = @jira_client.init_access_token(
        :oauth_verifier => params[:oauth_verifier]
      )

      session[:jira_auth] = {
        :access_token => access_token.token,
        :access_key => access_token.secret
      }

      session.delete(:request_token)
      session.delete(:request_secret)

      redirect '/'
    end

    # Initialize the JIRA session
    get '/login' do
      request_token = @jira_client.request_token
      session[:request_token] = request_token.token
      session[:request_secret] = request_token.secret
      redirect request_token.authorize_url
    end

    # Expire the JIRA session
    get '/logout' do
      session.delete(:jira_auth)
      redirect '/'
    end



  private
    def logged_in?
      !!session[:jira_auth]
    end

    def ops?
      return false unless logged_in?
      @myself['groups']['items'].each do |i|
        return true if i['name'] == settings.config[:ops_group]
      end
      return false
    end

    def one_day
      1 * 24 * 60 * 60 # Day * Hour * Minute * Second = Seconds / Day
    end

    def now
      Time.now # + 3 * one_day # DEBUG
    end

    def todays_date offset=0
      date  = now + offset
      date += one_day if date.saturday?
      date += one_day if date.sunday?
      return date
    end

    def stats_for issues, resolved_link, unresolved_link
      return {} unless logged_in?
      return {} unless issues

      resolved_issues, unresolved_issues = [], []

      issues.map! do |i|
        key = i['key']
        status = i['fields']['status']['name']
        resolution = i['fields']['resolution']['name'] rescue nil
        points = i['fields']['customfield_10002'].to_i

        issue = {
          key: key,
          status: status,
          resolution: resolution,
          points: points
        }

        if resolution.nil?
          unresolved_issues << issue
        else
          resolved_issues << issue
        end
      end

      {
        resolved: {
          number: resolved_issues.size,
          points: resolved_issues.map { |i| i[:points] }.reduce(0, :+),
          link: resolved_link
        },
        unresolved: {
          number: unresolved_issues.size,
          points: unresolved_issues.map { |i| i[:points] }.reduce(0, :+),
          link: unresolved_link
        }
      }
    end

    def items_in_current_sprint
      items_in_sprint current_sprint_num
    end

    def items_in_sprint num
      return [] unless logged_in?
      issues = []
      id = get_sprint(num)['id']
      @jira_client.Issue.jql("sprint = #{id}", max_results: 500).each do |i|
        issues << i.attrs
      end
      return issues
    end

    def asks_in_current_sprint
      asks_in_sprint current_sprint_num
    end

    def asks_in_sprint num
      return [] unless logged_in?
      issues = []
      @jira_client.Issue.jql("labels in (Sprint#{num})", max_results: 500).each do |i|
        issues << i.attrs
      end
      return issues
    end

    def sprints
      url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/sprintquery/#{settings.config[:agile_board]}"
      curl_request = Curl::Easy.http_get(url) do |curl|
        curl.headers['Accept'] = 'application/json'
        curl.headers['Content-Type'] = 'application/json'
        curl.http_auth_types = :basic
        curl.username = settings.config[:jira_user]
        curl.password = settings.config[:jira_pass]
        curl.verbose  = true
      end

      raw_response = curl_request.body_str
      begin
        data = JSON::parse(raw_response)
        return data['sprints']
      rescue
        $stderr.puts "Failed to parse response from JIRA: #{raw_response}"
      end
      return nil
    end

    def get_sprint num
      sprint = sprints.select { |s| s['name'] == "Sprint #{num}" }
      sprint_id = sprint.first['id']
      url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/rapid/charts/sprintreport?rapidViewId=#{settings.config[:agile_board]}&sprintId=#{sprint_id}"
      curl_request = Curl::Easy.http_get(url) do |curl|
        curl.headers['Accept'] = 'application/json'
        curl.headers['Content-Type'] = 'application/json'
        curl.http_auth_types = :basic
        curl.username = settings.config[:jira_user]
        curl.password = settings.config[:jira_pass]
        curl.verbose  = true
      end

      raw_response = curl_request.body_str
      begin
        data = JSON::parse(raw_response)
        contents = data.delete('contents')
        data = data.delete('sprint')
        return data.merge(contents)
      rescue
        $stderr.puts "Failed to parse response from JIRA: #{raw_response}"
      end
      return {}
    end

    def current_sprint_name sprint=current_sprint
      sprint.nil? ? nil : sprint['name'].gsub(/\s+/, '')
    end

    def current_sprint_num sprint=current_sprint
      sprint.nil? ? nil : sprint['name'].gsub(/\D+/, '')
    end

    def current_sprint_id sprint=current_sprint
      sprint.nil? ? nil : sprint['id']
    end

    def current_sprint keys=[ 'sprintsData', 'sprints', 0 ]
      url = "#{settings.config[:jira_url]}/rest/greenhopper/1.0/xboard/work/allData.json?rapidViewId=#{settings.config[:agile_board]}"
      curl_request = Curl::Easy.http_get(url) do |curl|
        curl.headers['Accept'] = 'application/json'
        curl.headers['Content-Type'] = 'application/json'
        curl.http_auth_types = :basic
        curl.username = settings.config[:jira_user]
        curl.password = settings.config[:jira_pass]
        curl.verbose  = true
      end

      raw_response = curl_request.body_str
      begin
        data = JSON::parse(raw_response)
        keys.each { |k| data = data[k] }
        return data unless data.nil?
      rescue
        $stderr.puts "Failed to parse response from JIRA: #{raw_response}"
      end

      return sprints.last
    end

    def today offset=0
      todays_date(offset).strftime '%Y-%m-%d'
    end

    def tomorrow
      today(one_day)
    end

    def name_for_today offset=0
      todays_date(offset).strftime '%A %^b %-d'
    end

    def name_for_tomorrow
      name_for_today(one_day)
    end

    def name_for_coming_week
      todays_date.strftime 'Week of %^b %-d'
    end

    def jiras_for date
      return [] unless logged_in?
      unless ops?
        return @jira_client.Issue.jql("due = #{date} AND project = #{settings.config[:project_name]} AND type != Change AND labels not in (OpsOnly)", max_results: 100)
      end
      return @jira_client.Issue.jql("due = #{date} AND project = #{settings.config[:project_name]} AND type != Change", max_results: 100)
    end

    def jira_count_for date
      jiras_for(date).length
    end

    def jira_count_for_today ; jira_count_for(today) end

    def jira_count_for_tomorrow ; jira_count_for(tomorrow) end

    def raw_classes_for jira
      classes = [ jira.fields['resolution'].nil? ? 'open' : 'closed' ]
      classes << jira.fields['assignee']['name'].downcase.gsub(/\W+/, '')
    end

    def classes_for jira
      raw_classes_for(jira).join(' ')
    end

    def sorting_key_for jira
      rcs = raw_classes_for(jira)
      idx = 1
      idx = 2 if rcs.include? 'denimcores'
      idx = 0 if rcs.include? 'closed'
      return "#{idx}-#{jira.key}"
    end

    def issues_for date
      jiras_for(date).sort_by do |jira|
        sorting_key_for(jira)
      end.reverse
    end

    def its_the_weekend?
      now.saturday? || now.sunday?
    end

    def room_for_new_jiras_for? date
      return true if ops?
      jira_count_for(date) < settings.config[:queue_size]
    end

    def date_for_new_jiras
      if now.hour < settings.config[:cutoff_hour] || its_the_weekend?
        return today if room_for_new_jiras_for? today
        return tomorrow if room_for_new_jiras_for? tomorrow
      else
        return tomorrow if room_for_new_jiras_for? tomorrow
      end
      return nil
    end

    def room_for_new_jiras?
      return true if ops?
      !date_for_new_jiras.nil?
    end

    def validate_room_for_new_jiras
      duedate = date_for_new_jiras
      return duedate unless duedate.nil?
      flash[:error] = [ "Sorry, there's is no room for new JIRAs" ]
      redirect '/'
    end

    def validate_jira_params
      flash[:error] = []
      flash[:error] << 'Summary is required' if params['jira-summary'].empty?
      redirect '/' unless flash[:error].empty?
      return [
        params['jira-component'],
        params['jira-summary'],
        params['jira-description'],
        !!params['jira-assign_to_me'],
        params['jira-epic'],
        !!params['jira-ops_only']
      ]
    end

    def create_jira duedate, component, summary, description, assign_to_me, epic, ops_only
      epic       = 'INF-3091' if epic.nil? # OpsAsk default epic
      assignee   = assign_to_me ? @me : settings.config[:assignee]
      components = []
      components = [ { name: component } ] unless component
      labels     = [ 'OpsAsk', current_sprint_name ].compact
      labels    << 'OpsOnly' if ops_only
      data       = {
        fields: {
          project: { key: settings.config[:project_key] },
          issuetype: { name: settings.config[:issue_type] },
          versions: [ { name: settings.config[:version] } ],
          duedate: duedate,
          summary: summary,
          description: description,
          components: components,
          assignee: { name: assignee },
          reporter: { name: @me },
          labels: labels,
          customfield_10002: 1, # Story Points = 1
          # customfield_10350: epic,
          customfield_10040: { id: '-1' } # Release Priority = None
        }
      }

      url = "#{settings.config[:jira_url]}/rest/api/latest/issue"
      curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
        curl.headers['Accept'] = 'application/json'
        curl.headers['Content-Type'] = 'application/json'
        curl.http_auth_types = :basic
        curl.username = settings.config[:jira_user]
        curl.password = settings.config[:jira_pass]
        curl.verbose  = true
      end

      raw_response = curl_request.body_str
      begin
        response = JSON::parse raw_response
      rescue
        $stderr.puts "Failed to parse response from JIRA: #{raw_response}"
        return nil
      end
      return response
    end

    def components
      return @project.components.map(&:name).select { |c| c =~ /^Ops/ }
    end

    def untracked_issues
      return [] unless logged_in?
      constraints = [
        "project = #{settings.config[:project_name]}",
        "due < #{today}",
        "resolution = unresolved",
        "assignee = denimcores"
      ].join(' AND ')
      @jira_client.Issue.jql(constraints, max_results: 100).sort_by do |jira|
        sorting_key_for(jira)
      end.reverse
    end

    def straggling_issues
      return [] unless logged_in?
      constraints = [
        "project = #{settings.config[:project_name]}",
        "due < #{today}",
        "labels in (OpsAsk)",
        "resolution = unresolved",
        "assignee != denimcores"
      ].join(' AND ')
      @jira_client.Issue.jql(constraints, max_results: 100).sort_by do |jira|
        sorting_key_for(jira)
      end.reverse
    end

    def epics
      data = {
        jql: "type = Epic AND project = #{settings.config[:project_name]}",
        startAt: 0,
        maxResults: 1000
      }

      url = "#{settings.config[:jira_url]}/rest/api/latest/search"
      curl_request = Curl::Easy.http_post(url, data.to_json) do |curl|
        curl.headers['Accept'] = 'application/json'
        curl.headers['Content-Type'] = 'application/json'
        curl.http_auth_types = :basic
        curl.username = settings.config[:jira_user]
        curl.password = settings.config[:jira_pass]
        curl.verbose  = true
      end

      raw_response = curl_request.body_str
      begin
        response = JSON::parse raw_response
      rescue
        $stderr.puts "Failed to parse response from JIRA: #{raw_response}"
        return nil
      end
      return response['issues'].map do |epic|
        {
          'key' => epic['key'],
          'name' => epic['fields']['customfield_10351'] || epic['fields']['summary']
        }
      end
    end

    def epic key
      url = "#{settings.config[:jira_url]}/rest/api/latest/issue/#{key}"
      curl_request = Curl::Easy.http_get(url) do |curl|
        curl.headers['Accept'] = 'application/json'
        curl.headers['Content-Type'] = 'application/json'
        curl.http_auth_types = :basic
        curl.username = settings.config[:jira_user]
        curl.password = settings.config[:jira_pass]
        curl.verbose  = true
      end

      raw_response = curl_request.body_str
      begin
        response = JSON::parse raw_response
      rescue
        $stderr.puts "Failed to parse response from JIRA: #{raw_response}"
        return nil
      end
      return {
        'key' => response['key'],
        'name' => response['fields']['customfield_10351'] || response['fields']['summary']
      }
    end
  end
end