# frozen_string_literal: true

class GoogleMaps::PhoneTakeoutImporter
  include Imports::Broadcaster
  include Imports::FileLoader

  attr_reader :import, :user_id, :file_path

  def initialize(import, user_id, file_path = nil)
    @import   = import
    @user_id  = user_id
    @file_path = file_path
  end

  BATCH_SIZE = 1000

  def call
    points_data = parse_json.compact.map do |point_data|
      point_data.merge(
        import_id: import.id,
        topic: 'Google Maps Phone Timeline Export',
        tracker_id: 'google-maps-phone-timeline-export',
        user_id: user_id,
        created_at: Time.current,
        updated_at: Time.current
      )
    end

    points_data.each_slice(BATCH_SIZE).with_index do |batch, batch_index|
      bulk_insert_points(batch)
      broadcast_import_progress(import, (batch_index + 1) * BATCH_SIZE)
    end
  end

  private

  def parse_json
    # location-history.json could contain an array of data points
    # or an object with semanticSegments, rawSignals and rawArray
    semantic_segments = []
    raw_signals       = []
    raw_array         = []

    json = load_json_data

    if json.is_a?(Array)
      raw_array = parse_raw_array(json)
    else
      semantic_segments = parse_semantic_segments(json['semanticSegments']) if json['semanticSegments']
      raw_signals = parse_raw_signals(json['rawSignals']) if json['rawSignals']
    end

    semantic_segments + raw_signals + raw_array
  end

  def parse_coordinates(coordinates)
    if coordinates.include?('°')
      coordinates.split(', ').map { _1.chomp('°') }
    else
      coordinates.delete('geo:').split(',')
    end
  end

  def point_hash(lat, lon, timestamp, raw_data)
    {
      lonlat: "POINT(#{lon.to_f} #{lat.to_f})",
      timestamp:,
      raw_data:,
      accuracy: raw_data['accuracyMeters'],
      altitude: raw_data['altitudeMeters'],
      velocity: raw_data['speedMetersPerSecond']
    }
  end

  def parse_visit_place_location(data_point)
    lat, lon = parse_coordinates(data_point['visit']['topCandidate']['placeLocation'])
    timestamp = DateTime.parse(data_point['startTime']).to_i

    point_hash(lat, lon, timestamp, data_point)
  end

  def parse_activity(data_point)
    start_lat, start_lon = parse_coordinates(data_point['activity']['start'])
    start_timestamp = DateTime.parse(data_point['startTime']).to_i

    end_lat, end_lon = parse_coordinates(data_point['activity']['end'])
    end_timestamp = DateTime.parse(data_point['endTime']).to_i

    [
      point_hash(start_lat, start_lon, start_timestamp, data_point),
      point_hash(end_lat, end_lon, end_timestamp, data_point)
    ]
  end

  def parse_timeline_path(data_point)
    data_point['timelinePath'].map do |point|
      lat, lon = parse_coordinates(point['point'])
      start_time = DateTime.parse(data_point['startTime'])
      offset = point['durationMinutesOffsetFromStartTime']

      timestamp = start_time
      timestamp += offset.to_i.minutes if offset.present?

      point_hash(lat, lon, timestamp, data_point)
    end
  end

  def parse_semantic_visit(segment)
    lat, lon = parse_coordinates(segment['visit']['topCandidate']['placeLocation']['latLng'])
    timestamp = DateTime.parse(segment['startTime']).to_i

    point_hash(lat, lon, timestamp, segment)
  end

  def parse_semantic_activity(segment)
    start_lat, start_lon = parse_coordinates(segment['activity']['start']['latLng'])
    start_timestamp = DateTime.parse(segment['startTime']).to_i
    end_lat, end_lon = parse_coordinates(segment['activity']['end']['latLng'])
    end_timestamp = DateTime.parse(segment['endTime']).to_i

    [
      point_hash(start_lat, start_lon, start_timestamp, segment),
      point_hash(end_lat, end_lon, end_timestamp, segment)
    ]
  end

  def parse_semantic_timeline_path(segment)
    segment['timelinePath'].map do |point|
      lat, lon = parse_coordinates(point['point'])
      timestamp = DateTime.parse(point['time']).to_i

      point_hash(lat, lon, timestamp, segment)
    end
  end

  def parse_raw_array(raw_data)
    raw_data.flat_map do |data_point|
      if data_point.dig('visit', 'topCandidate', 'placeLocation')
        parse_visit_place_location(data_point)
      elsif data_point.dig('activity', 'start') && data_point.dig('activity', 'end')
        parse_activity(data_point)
      elsif data_point['timelinePath']
        parse_timeline_path(data_point)
      end
    end.compact
  end

  def parse_semantic_segments(semantic_segments)
    semantic_segments.flat_map do |segment|
      if segment.key?('timelinePath')
        parse_semantic_timeline_path(segment)
      elsif segment.key?('visit')
        parse_semantic_visit(segment)
      else # activities
        # Some activities don't have start latLng
        next if segment.dig('activity', 'start', 'latLng').nil?

        parse_semantic_activity(segment)
      end
    end
  end

  def parse_raw_signals(raw_signals)
    raw_signals.flat_map do |segment|
      next unless segment.dig('position', 'LatLng')

      lat, lon = parse_coordinates(segment['position']['LatLng'])
      timestamp = DateTime.parse(segment['position']['timestamp']).to_i

      point_hash(lat, lon, timestamp, segment)
    end
  end

  def bulk_insert_points(batch)
    unique_batch = batch.uniq { |record| [record[:lonlat], record[:timestamp], record[:user_id]] }

    # rubocop:disable Rails/SkipsModelValidations
    Point.upsert_all(
      unique_batch,
      unique_by: %i[lonlat timestamp user_id],
      returning: false,
      on_duplicate: :skip
    )
    # rubocop:enable Rails/SkipsModelValidations
  rescue StandardError => e
    create_notification("Failed to process phone takeout batch: #{e.message}")
  end

  def create_notification(message)
    Notification.create!(
      user_id: user_id,
      title: 'Google Maps Phone Takeout Import Error',
      content: message,
      kind: :error
    )
  end
end
