class FetchJmaContentJob < ApplicationJob
  queue_as :default

  File.open(Rails.root.join('config', 'jma-point2area-map.json')) do |f|
    POINT2AREA_MAP = JSON.load(f)
  end

  FC_UPDATE_TYPE_MAP = {
    "天気" => :weather,
    "降水確率" => :pop,
    "朝の最低気温" => :temp_min,
    "日中の最高気温" => :temp_max,
  }

  def perform(signal)
    if signal.created_at + 24.hours < Time.now
      logger.warn "Queued signal ##{signal.id} has been expired (24 hours), skip!"
      signal.update_attribute :status, :ignored
      return
    end

    hc = Faraday.new

    count = 0

    signal_doc = Nokogiri::XML signal.body
    signal_doc.remove_namespaces!
    signal_doc.xpath('/feed/entry').each do |entry|
      if (title = entry.xpath('title').text.to_s.strip) != '府県天気予報'
        logger.info "Skip entry #{title}"
        next
      end
      entry.xpath('link').each do |link|
        url = link.attr('href')
        url.blank? and next
        logger.info "Fetching #{url}..."
        resp = hc.get url
        if resp.status != 200
          logger.error "Failed to fetch #{url} on signal##{signal.id}: status=#{resp.status} #{resp.reason_phrase}"
          raise "Invalid HTTP Status #{resp.status}"
        end

        logger.debug "Parsing forecast XML..."
        fc_doc = Nokogiri::XML resp.body
        _update_fc_by_xml(fc_doc)
        count += 1 # TODO
      end
    end

    signal.update_attribute :status, count < 1 ? :ignored : :completed
  end

  def _update_fc_by_xml(fc_doc)
    fc_doc = fc_doc.dup
    fc_doc.remove_namespaces!
    Time.use_zone('Asia/Tokyo') do
      __update_area(fc_doc)
    end
  end

  def __update_area(fc_doc)
    fc_report = fc_doc.xpath('/Report')
    fc_status = fc_report.xpath('Control/Status').text
    if fc_status != "通常"
      logger.warning "Skip forecast XML, status=#{fc_status} (#{url})"
      return
    end
    fc_pref = fc_report.xpath('Head/Title').text.strip.gsub(/府県天気予報$/, '')
    fc_time = Time.parse(fc_report.xpath('Head/ReportDateTime').text)
    logger.debug "Forecast time: #{fc_time}"
    Area.transaction do
      changed_area_ids = []
      fc_report.xpath('Body/MeteorologicalInfos[@type="区域予報"]/TimeSeriesInfo').each do |ti|
        time_defs = __get_time_defs(ti)

        ti.xpath('Item/Kind/Property/Type').each do |fc_type|
          FC_UPDATE_TYPE_MAP.keys.member?(fc_type.text) or next
          fc_props = fc_type.parent
          fc_area  = fc_props.parent.parent.xpath('Area')
          fc_area_code = fc_area.xpath('Code').text
          fc_area_name = fc_area.xpath('Name').text
          area = Area.find_or_initialize_by ns: 'jma', code: fc_area_code
          area.country = 'ja'
          area.timezone = 'Asia/Tokyo'
          area.name = "#{fc_pref} #{fc_area_name}"
          area.pref = fc_pref
          area.save!

          changed_area_ids << area.id

          send "__update_fc_#{FC_UPDATE_TYPE_MAP[fc_type.text]}", area, fc_time, time_defs, fc_props
        end
      end

      fc_report.xpath('Body/MeteorologicalInfos[@type="地点予報"]/TimeSeriesInfo').each do |ti|
        time_defs = __get_time_defs(ti)

        ti.xpath('Item/Kind/Property/Type').each do |fc_type|
          FC_UPDATE_TYPE_MAP.keys.member?(fc_type.text) or next
          fc_props = fc_type.parent
          fc_st    = fc_props.parent.parent.xpath('Station')
          fc_st_code = fc_st.xpath('Code').text
          fc_st_name = fc_st.xpath('Name').text
          fc_area = POINT2AREA_MAP[fc_st_code]
          unless fc_area
            raise "Failed to get JMA area from Amedas station! (station: #{fc_st_name} #{fc_st_code})"
          end
          area = Area.find_or_initialize_by ns: 'jma', code: fc_area['code'], country: 'ja'
          area.name = fc_area['label']
          area.pref = fc_area['pref']
          area.save!

          changed_area_ids << area.id

          send "__update_fc_#{FC_UPDATE_TYPE_MAP[fc_type.text]}", area, fc_time, time_defs, fc_props
        end
      end
      changed_area_ids.present? and
        Forecast.where("area_id IN (?) AND date < ?", changed_area_ids, (Time.zone.now - 1.day).to_date).destroy_all
    end # Area.transaction
  end

  def __get_time_defs(time_series_info)
    time_defs = {}
    time_series_info.xpath('TimeDefines/TimeDefine').each do |td|
      time_defs[td.attr('timeId')] = Time.parse(td.xpath('DateTime').text)
    end
    time_defs
  end


  def __update_fc_weather(area, reported_at, time_defs, props_node)
    props_node.xpath('DetailForecast/WeatherForecastPart').each do |f|
      labels = []
      suppl = nil
      sym = ""
      hour = time_defs[f.attr('refID')].hour
      f.xpath("Base|Becoming|Temporary").each do |wp|
        cur_label = wp.xpath("TimeModifier").text + wp.xpath("Weather").text
        cur_label.blank? or labels.push(cur_label.tr("　 \n\r", ''))
        sym.length < 2 && wp.xpath("Weather").text.present? and
          sym += __weather_str_to_sym(wp.xpath("Weather").text, hour)
      end

      if f.xpath("SubArea")
        if f.xpath("SubArea/Sentence") && f.xpath("SubArea/Sentence").text.strip.present?
          suppl = f.xpath("SubArea/Sentence").text.tr("　 \n\r", '')
        end
      end
      sym.gsub!(/(.)\1/, '\1')

      fc = Forecast.find_or_initialize_by area_id: area.id, date: time_defs[f.attr('refID')].to_date
      fc.update_attributes(weather_label: labels.join(' '),
                           weather_symbol: sym,
                           weather_suppl: suppl,
                           reported_at: reported_at,
                          )
      fc.save!
    end
  end

  def __weather_str_to_sym(str, hour)
    case str
    when /^(晴れ|晴|はれ)/
      if hour >= 16
        Forecast::WEATHER_SYM_CLEAR_N
      else
        Forecast::WEATHER_SYM_CLEAR_D
      end
    when /^(?:暴風)?(雨|あめ)/
      Forecast::WEATHER_SYM_RAIN
    when /^(?:暴風)?(雪|ゆき)/
      Forecast::WEATHER_SYM_SNOW
    when /^(曇り|曇|くもり)/
      Forecast::WEATHER_SYM_CLOUD
    when /^(雷|かみなり)/
      Forecast::WEATHER_SYM_THUNDER
    else
      logger.error "Failed to resolve weather symbol for '#{str}'"
      ""
    end
  end

  def __update_fc_pop(area, reported_at, time_defs, props_node)
    fcs = Hash.new do |h1, area|
      h1[area] = Hash.new do |h2, date|
        fc = Forecast.find_or_initialize_by area_id: area.id, date: date
        fc.pop = nil
        h2[date] = fc
      end
    end
    props_node.xpath('ProbabilityOfPrecipitationPart/ProbabilityOfPrecipitation').each do |pop_node|
      pop_node.attr('unit') != "%" and raise "Unuknown pop unit #{pop.attr('unit')}!"
      pop_node.text.blank? and next
      fc = fcs[area][time_defs[pop_node.attr('refID')].to_date]
      fc.reported_at = reported_at
      pop = pop_node.text.to_i
      fc.pop ||= pop
      fc.pop < pop and fc.pop = pop
    end
    fcs.values.each {|h| h.values.each{|fc| fc.pop && fc.save!} }
  end

  def ___update_fc_temp(attr, area, reported_at, time_defs, props_node)
    props_node.xpath('TemperaturePart/Temperature').each do |temp_node|
      fc = Forecast.find_or_initialize_by area_id: area.id, date: time_defs[temp_node.attr('refID')].to_date
      fc.update reported_at: reported_at, attr => temp_node.text.to_i
    end
  end

  def __update_fc_temp_min(area, reported_at, time_defs, props_node)
    ___update_fc_temp(:temp_min, area, reported_at, time_defs, props_node)
  end

  def __update_fc_temp_max(area, reported_at, time_defs, props_node)
    ___update_fc_temp(:temp_max, area, reported_at, time_defs, props_node)
  end
end
