# frozen_string_literal: true

module Distanceable
  extend ActiveSupport::Concern

  module ClassMethods
    def total_distance(points = nil, unit = :km)
      if points.nil?
        calculate_distance_for_relation(unit)
      else
        calculate_distance_for_array(points, unit)
      end
    end

    # In-memory distance calculation using Geocoder (no SQL dependency)
    def calculate_distance_for_array_geocoder(points, unit = :km)
      unless ::DISTANCE_UNITS.key?(unit.to_sym)
        raise ArgumentError, "Invalid unit. Supported units are: #{::DISTANCE_UNITS.keys.join(', ')}"
      end

      return 0 if points.length < 2

      total_meters = points.each_cons(2).sum do |p1, p2|
        # Extract coordinates from lonlat (source of truth)
        begin
          # Check if lonlat exists and is valid
          if p1.lonlat.nil? || p2.lonlat.nil?
            Rails.logger.warn "Skipping distance calculation for points with nil lonlat: p1(#{p1.id}), p2(#{p2.id})"
            next 0
          end

          lat1, lon1 = p1.lat, p1.lon
          lat2, lon2 = p2.lat, p2.lon
          
          # Check for nil coordinates extracted from lonlat
          if lat1.nil? || lon1.nil? || lat2.nil? || lon2.nil?
            Rails.logger.warn "Skipping distance calculation for points with nil extracted coordinates: p1(#{p1.id}: #{lat1}, #{lon1}), p2(#{p2.id}: #{lat2}, #{lon2})"
            next 0
          end

          # Check for NaN or infinite coordinates
          if [lat1, lon1, lat2, lon2].any? { |coord| !coord.finite? }
            Rails.logger.warn "Skipping distance calculation for points with invalid coordinates: p1(#{p1.id}: #{lat1}, #{lon1}), p2(#{p2.id}: #{lat2}, #{lon2})"
            next 0
          end

          # Check for valid latitude/longitude ranges
          if lat1.abs > 90 || lat2.abs > 90 || lon1.abs > 180 || lon2.abs > 180
            Rails.logger.warn "Skipping distance calculation for points with out-of-range coordinates: p1(#{p1.id}: #{lat1}, #{lon1}), p2(#{p2.id}: #{lat2}, #{lon2})"
            next 0
          end

          distance_km = Geocoder::Calculations.distance_between(
            [lat1, lon1],
            [lat2, lon2],
            units: :km
          )

          # Check if Geocoder returned NaN or infinite value
          if !distance_km.finite?
            Rails.logger.warn "Geocoder returned invalid distance (#{distance_km}) for points: p1(#{p1.id}: #{lat1}, #{lon1}), p2(#{p2.id}: #{lat2}, #{lon2})"
            next 0
          end

          distance_km * 1000 # Convert km to meters
        rescue StandardError => e
          Rails.logger.error "Error extracting coordinates from lonlat for points #{p1.id}, #{p2.id}: #{e.message}"
          next 0
        end
      end

      result = total_meters.to_f / ::DISTANCE_UNITS[unit.to_sym]

      # Final validation of result
      if !result.finite?
        Rails.logger.error "Final distance calculation resulted in invalid value (#{result}) for #{points.length} points"
        return 0
      end

      result
    end

    private

    def calculate_distance_for_relation(unit)
      unless ::DISTANCE_UNITS.key?(unit.to_sym)
        raise ArgumentError, "Invalid unit. Supported units are: #{::DISTANCE_UNITS.keys.join(', ')}"
      end

      distance_in_meters = connection.select_value(<<-SQL.squish)
        WITH points_with_previous AS (
          SELECT
            lonlat,
            LAG(lonlat) OVER (ORDER BY timestamp) as prev_lonlat
          FROM (#{to_sql}) AS points
        )
        SELECT COALESCE(
          SUM(
            ST_Distance(
              lonlat::geography,
              prev_lonlat::geography
            )
          ),
          0
        )
        FROM points_with_previous
        WHERE prev_lonlat IS NOT NULL
      SQL

      distance_in_meters.to_f / ::DISTANCE_UNITS[unit.to_sym]
    end

    def calculate_distance_for_array(points, unit = :km)
      unless ::DISTANCE_UNITS.key?(unit.to_sym)
        raise ArgumentError, "Invalid unit. Supported units are: #{::DISTANCE_UNITS.keys.join(', ')}"
      end

      return 0 if points.length < 2

      total_meters = calculate_batch_distances(points).sum

      total_meters.to_f / ::DISTANCE_UNITS[unit.to_sym]
    end

    def calculate_batch_distances(points)
      return [] if points.length < 2

      point_pairs = points.each_cons(2).to_a
      return [] if point_pairs.empty?

      # Create parameterized placeholders for VALUES clause using ? placeholders
      values_placeholders = point_pairs.map do |_|
        "(?, ST_GeomFromEWKT(?)::geography, ST_GeomFromEWKT(?)::geography)"
      end.join(', ')

      # Flatten parameters: [pair_id, lonlat1, lonlat2, pair_id, lonlat1, lonlat2, ...]
      params = point_pairs.flat_map.with_index do |(p1, p2), index|
        [index, p1.lonlat, p2.lonlat]
      end

      # Single query to calculate all distances using parameterized query
      sql_with_params = ActiveRecord::Base.sanitize_sql_array([<<-SQL.squish] + params)
        WITH point_pairs AS (
          SELECT
            pair_id,
            point1,
            point2
          FROM (VALUES #{values_placeholders}) AS t(pair_id, point1, point2)
        )
        SELECT
          pair_id,
          ST_Distance(point1, point2) as distance_meters
        FROM point_pairs
        ORDER BY pair_id
      SQL

      results = connection.select_all(sql_with_params)

      # Return array of distances in meters
      results.map { |row| row['distance_meters'].to_f }
    end
  end

  def distance_to(other_point, unit = :km)
    unless ::DISTANCE_UNITS.key?(unit.to_sym)
      raise ArgumentError, "Invalid unit. Supported units are: #{::DISTANCE_UNITS.keys.join(', ')}"
    end

    other_lonlat = extract_point(other_point)
    return nil if other_lonlat.nil?

    # Calculate distance in meters using PostGIS
    distance_in_meters = self.class.connection.select_value(<<-SQL.squish)
      SELECT ST_Distance(
        ST_GeomFromEWKT('#{lonlat}')::geography,
        ST_GeomFromEWKT('#{other_lonlat}')::geography
      )
    SQL

    # Convert to requested unit
    distance_in_meters.to_f / ::DISTANCE_UNITS[unit.to_sym]
  end

  # In-memory distance calculation using Geocoder (no SQL dependency)
  def distance_to_geocoder(other_point, unit = :km)
    unless ::DISTANCE_UNITS.key?(unit.to_sym)
      raise ArgumentError, "Invalid unit. Supported units are: #{::DISTANCE_UNITS.keys.join(', ')}"
    end

    begin
      # Extract coordinates from lonlat (source of truth) for current point
      if lonlat.nil?
        Rails.logger.warn "Cannot calculate distance: current point has nil lonlat"
        return 0
      end

      current_lat, current_lon = lat, lon
      
      other_lat, other_lon = case other_point
                             when Array
                               [other_point[0], other_point[1]]
                             else
                               # For other Point objects, extract from their lonlat too
                               if other_point.respond_to?(:lonlat) && other_point.lonlat.nil?
                                 Rails.logger.warn "Cannot calculate distance: other point has nil lonlat"
                                 return 0
                               end
                               [other_point.lat, other_point.lon]
                             end
      
      # Check for nil coordinates extracted from lonlat
      if current_lat.nil? || current_lon.nil? || other_lat.nil? || other_lon.nil?
        Rails.logger.warn "Cannot calculate distance: nil coordinates detected - current(#{current_lat}, #{current_lon}), other(#{other_lat}, #{other_lon})"
        return 0
      end

      # Check for NaN or infinite coordinates
      coords = [current_lat, current_lon, other_lat, other_lon]
      if coords.any? { |coord| !coord.finite? }
        Rails.logger.warn "Cannot calculate distance: invalid coordinates detected - current(#{current_lat}, #{current_lon}), other(#{other_lat}, #{other_lon})"
        return 0
      end

      # Check for valid latitude/longitude ranges
      if current_lat.abs > 90 || other_lat.abs > 90 || current_lon.abs > 180 || other_lon.abs > 180
        Rails.logger.warn "Cannot calculate distance: out-of-range coordinates - current(#{current_lat}, #{current_lon}), other(#{other_lat}, #{other_lon})"
        return 0
      end

      distance_km = Geocoder::Calculations.distance_between(
        [current_lat, current_lon],
        [other_lat, other_lon],
        units: :km
      )

      # Check if Geocoder returned valid distance
      if !distance_km.finite?
        Rails.logger.warn "Geocoder returned invalid distance (#{distance_km}) for points: current(#{current_lat}, #{current_lon}), other(#{other_lat}, #{other_lon})"
        return 0
      end

      result = (distance_km * 1000).to_f / ::DISTANCE_UNITS[unit.to_sym]

      # Final validation
      if !result.finite?
        Rails.logger.error "Final distance calculation resulted in invalid value (#{result})"
        return 0
      end

      result
    rescue StandardError => e
      Rails.logger.error "Error calculating distance from lonlat: #{e.message}"
      0
    end
  end

  private

  def extract_point(point)
    case point
    when Array
      unless point.length == 2
        raise ArgumentError,
              'Coordinates array must contain exactly 2 elements [latitude, longitude]'
      end

      RGeo::Geographic.spherical_factory(srid: 4326).point(point[1], point[0])
    when self.class
      point.lonlat
    end
  end
end
