require 'time'
require 'date'

class PostMetadataFetcher
  TIMESTAMP_PATTERN =
      /\A(\d{4})-(\d{2})-(\d{2})T(\d{2})
        (?::(\d{2})(?::(\d{2})(?:\.(\d+))?)?)?
        (Z|[+-]\d{2}:?\d{2})?\z/x

  def self.fetch raw_url
    uri, = Preview::UrlSafety.validate(raw_url)
    response = Preview::HttpFetcher.fetch(
        uri.to_s,
        max_bytes: Preview::ThumbnailFetcher::HTML_MAX_BYTES)
    metadata = Preview::HtmlMetadataExtractor.extract(response)
    document = Nokogiri::HTML.parse(response.body)
    content = lambda { |name|
      document
        .at_css("meta[property='#{ name }'], meta[name='#{ name }']")
        &.[]('content')
        &.strip
        &.presence
    }
    duration = content.call('og:video:duration') || content.call('video:duration')
    published = content.call('article:published_time') || content.call('date')
    created_range = original_created_range(published)
    platform_tags = platform_tags(uri)
    { title: metadata[:title],
      thumbnail_base:
          Preview::KnownSiteExtractor.thumbnail_url(uri) || metadata[:image_url],
      original_created_from: serialise_time(created_range&.first),
      original_created_before: serialise_time(created_range&.last),
      duration: serialise_duration(duration),
      display_tags: display_tags(platform_tags),
      tags: platform_tags.join(' ') }
  end

  def self.platform_tags uri
    return ['動画', 'YouTube'] if Preview::KnownSiteExtractor.youtube_video_id(uri)
    return ['動画', 'ニコニコ'] if Preview::KnownSiteExtractor.niconico_video_id(uri)

    []
  end

  def self.display_tags names
    return [] if names.blank?

    existing_tags =
        Tag
        .joins(:tag_name)
        .where(tag_names: { name: names })
        .index_by(&:name)

    names.map { |name|
      tag = existing_tags[name]
      {
          name: name,
          category: (tag&.category || 'meta'),
          section_literals: [] }
    }
  end

  def self.original_created_range value
    return nil if value.blank?

    raw = value.to_s.strip
    from, before =
        case raw
        when /\A(\d{4})\z/
          year = Regexp.last_match(1).to_i
          from = Time.zone.local(year, 1, 1)
          [from, from + 1.year]
        when /\A(\d{4})-(\d{2})\z/
          year = Regexp.last_match(1).to_i
          month = Regexp.last_match(2).to_i
          return nil unless month.between?(1, 12)

          from = Time.zone.local(year, month, 1)
          [from, from + 1.month]
        when /\A(\d{4})-(\d{2})-(\d{2})\z/
          year = Regexp.last_match(1).to_i
          month = Regexp.last_match(2).to_i
          day = Regexp.last_match(3).to_i
          return nil unless Date.valid_date?(year, month, day)

          from = Time.zone.local(year, month, day)
          [from, from + 1.day]
        else
          parse_timestamp_range(raw)
        end
    return nil if from.nil? || before.nil?

    [from, before]
  rescue ArgumentError, TypeError
    nil
  end

  def self.parse_timestamp_range raw
    match = raw.match(TIMESTAMP_PATTERN)
    return nil unless match

    year = match[1].to_i
    month = match[2].to_i
    day = match[3].to_i
    hour = match[4].to_i
    minute = match[5]&.to_i || 0
    second = match[6]&.to_i || 0
    fraction = match[7]
    offset = match[8]
    return nil unless valid_timestamp_components?(year, month, day, hour, minute, second)
    return nil unless valid_offset?(offset)

    nanoseconds = parse_nanoseconds(fraction)
    timestamp =
        if offset.present?
          Time.new(
              year,
              month,
              day,
              hour,
              minute,
              second + Rational(nanoseconds, 1_000_000_000),
              parse_offset(offset)).in_time_zone
        else
          Time.zone.local(year, month, day, hour, minute, second).change(nsec: nanoseconds)
        end

    from = timestamp.change(sec: 0, nsec: 0)
    before =
        if match[5].nil?
          from + 1.hour
        else
          from + 1.minute
        end
    [from, before]
  end

  def self.parse_nanoseconds value
    return 0 if value.blank?

    digits = value[0, 9].ljust(9, '0')
    Integer(digits, 10)
  end

  def self.parse_offset value
    return '+00:00' if value == 'Z'

    value.match?(/\A[+-]\d{2}:\d{2}\z/) ? value : "#{ value[0, 3] }:#{ value[3, 2] }"
  end

  def self.serialise_duration value
    seconds = Float(value)
    return nil unless seconds.positive?

    milliseconds = (seconds * 1_000).round
    seconds_string = (milliseconds / 1_000.0).to_s
    seconds_string.end_with?('.0') ? seconds_string.delete_suffix('.0') : seconds_string
  rescue ArgumentError, TypeError
    nil
  end

  def self.valid_timestamp_components? year, month, day, hour, minute, second
    return false unless Date.valid_date?(year, month, day)
    return false unless hour.between?(0, 23)
    return false unless minute.between?(0, 59)
    return false unless second.between?(0, 59)

    true
  end

  def self.valid_offset? value
    return true if value.nil? || value == 'Z'

    match = value.match(/\A([+-])(\d{2}):?(\d{2})\z/)
    return false if match.nil?

    hours = match[2].to_i
    minutes = match[3].to_i
    hours.between?(0, 23) && minutes.between?(0, 59)
  end

  def self.serialise_time value
    return nil if value.nil?

    value.nsec.zero? ? value.iso8601 : value.iso8601(9)
  end

  private_class_method :platform_tags,
                       :display_tags,
                       :original_created_range,
                       :parse_timestamp_range,
                       :parse_nanoseconds,
                       :parse_offset,
                       :serialise_duration,
                       :valid_timestamp_components?,
                       :valid_offset?,
                       :serialise_time
end
