# frozen_string_literal: true

module Visits
  # Detects potential visits from a collection of tracked points
  class Detector
    MINIMUM_VISIT_DURATION = 3.minutes
    MAXIMUM_VISIT_GAP = 30.minutes
    MINIMUM_POINTS_FOR_VISIT = 2

    attr_reader :points, :place_name_suggester

    def initialize(points)
      @points = points
      @place_name_suggester = Visits::Names::Suggester
    end

    def detect_potential_visits
      visits = []
      current_visit = nil

      points.each do |point|
        if current_visit.nil?
          current_visit = initialize_visit(point)
          next
        end

        if belongs_to_current_visit?(point, current_visit)
          current_visit[:points] << point
          current_visit[:end_time] = point.timestamp
        else
          visits << finalize_visit(current_visit) if valid_visit?(current_visit)
          current_visit = initialize_visit(point)
        end
      end

      # Handle the last visit
      visits << finalize_visit(current_visit) if current_visit && valid_visit?(current_visit)

      visits
    end

    private

    def initialize_visit(point)
      {
        start_time: point.timestamp,
        end_time: point.timestamp,
        center_lat: point.lat,
        center_lon: point.lon,
        points: [point]
      }
    end

    def belongs_to_current_visit?(point, visit)
      time_gap = point.timestamp - visit[:end_time]
      return false if time_gap > MAXIMUM_VISIT_GAP

      # Calculate distance from visit center
      distance = Geocoder::Calculations.distance_between(
        [visit[:center_lat], visit[:center_lon]],
        [point.lat, point.lon],
        units: :km
      )

      # Dynamically adjust radius based on visit duration
      max_radius = calculate_max_radius(visit[:end_time] - visit[:start_time])

      distance <= max_radius
    end

    def calculate_max_radius(duration_seconds)
      # Start with a small radius for short visits, increase for longer stays
      # but cap it at a reasonable maximum
      base_radius = 0.05 # 50 meters
      duration_hours = duration_seconds / 3600.0
      [base_radius * (1 + Math.log(1 + duration_hours)), 0.5].min # Cap at 500 meters
    end

    def valid_visit?(visit)
      duration = visit[:end_time] - visit[:start_time]
      visit[:points].size >= MINIMUM_POINTS_FOR_VISIT && duration >= MINIMUM_VISIT_DURATION
    end

    def finalize_visit(visit)
      points = visit[:points]
      center = calculate_center(points)

      visit.merge(
        duration: visit[:end_time] - visit[:start_time],
        center_lat: center[0],
        center_lon: center[1],
        radius: calculate_visit_radius(points, center),
        suggested_name: suggest_place_name(points) || fetch_place_name(center)
      )
    end

    def calculate_center(points)
      lat_sum = points.sum(&:lat)
      lon_sum = points.sum(&:lon)
      count = points.size.to_f

      [lat_sum / count, lon_sum / count]
    end

    def calculate_visit_radius(points, center)
      max_distance = points.map do |point|
        Geocoder::Calculations.distance_between(center, [point.lat, point.lon], units: :km)
      end.max

      # Convert to meters and ensure minimum radius
      [(max_distance * 1000), 15].max
    end

    def suggest_place_name(points)
      place_name_suggester.new(points).call
    end

    def fetch_place_name(center)
      Visits::Names::Fetcher.new(center).call
    end
  end
end
