# frozen_string_literal: true

# NetworkRange - Unified IPv4/IPv6 network range management
#
# Uses PostgreSQL's inet type to handle both IPv4 and IPv4 networks seamlessly.
# Provides network intelligence data including ASN, company, geographic info,
# and classification flags (datacenter, proxy, VPN).
class NetworkRange < ApplicationRecord
  # Sources for network range creation
  SOURCES = %w[api_imported user_created manual auto_generated inherited geolite_asn geolite_country].freeze

  # Associations
  has_many :rules, dependent: :destroy
  belongs_to :user, optional: true

  # Validations
  validates :network, presence: true, uniqueness: true
  validates :source, inclusion: { in: SOURCES }
  validates :asn, numericality: { greater_than: 0 }, allow_blank: true

  # Scopes
  scope :ipv4, -> { where("family(network) = 4") }
  scope :ipv6, -> { where("family(network) = 6") }
  scope :by_country, ->(country) { where(country: country) }
  scope :by_company, ->(company) { where(company: company) }
  scope :by_asn, ->(asn) { where(asn: asn) }
  scope :datacenter, -> { where(is_datacenter: true) }
  scope :proxy, -> { where(is_proxy: true) }
  scope :vpn, -> { where(is_vpn: true) }
  scope :user_created, -> { where(source: 'user_created') }
  scope :api_imported, -> { where(source: 'api_imported') }
  scope :geolite_imported, -> { where(source: ['geolite_asn', 'geolite_country']) }
  scope :geolite_asn, -> { where(source: 'geolite_asn') }
  scope :geolite_country, -> { where(source: 'geolite_country') }
  scope :with_events, -> { where("events_count > 0") }
  scope :most_active, -> { order(events_count: :desc) }

  # Callbacks
  before_validation :set_default_source
  # after_save :update_children_inheritance!, if: :should_update_children_inheritance?  # Disabled for now

  # Virtual attribute for CIDR notation
  def cidr
    network.to_s
  end

  def cidr=(new_cidr)
    self.network = new_cidr
  end

  # Network properties
  def prefix_length
    # Get prefix length from IPAddr object
    network.prefix
  end

  def network_address
    # Use PostgreSQL's host function or get from IPAddr object
    network.to_s
  end

  def cidr
    # Return full CIDR notation
    "#{network_address}/#{prefix_length}"
  end

  def broadcast_address
    # Use PostgreSQL's broadcast function
    result = self.class.connection.execute("SELECT broadcast('#{network.to_s}')").first
    result&.values&.first
  end

  def family
    # Check if it's IPv4 or IPv6 by looking at the address
    addr = network.to_s.split('/').first
    addr.include?(':') ? 6 : 4
  end

  def virtual?
    # Virtual networks are unsaved instances (not persisted to database)
    !persisted?
  end

  def ipv4?
    family == 4
  end

  def ipv6?
    family == 6
  end

  # Network containment and overlap operations
  def contains_ip?(ip_string)
    # Use Postgres >>= operator for containment
    self.class.where("network >>= ?::inet", ip_string).exists?
  rescue => e
    Rails.logger.error "Error checking IP containment: #{e.message}"
    false
  end

  def contains_network?(other_cidr)
    other_network = IPAddr.new(other_cidr)
    network_range = IPAddr.new(network)
    network_range.include?(other_network)
  rescue IPAddr::InvalidAddressError
    false
  end

  def overlaps?(other_cidr)
    network_range = IPAddr.new(network)
    other_network = IPAddr.new(other_cidr)
    network_range.include?(other_network) || other_network.include?(network_range)
  rescue IPAddr::InvalidAddressError
    false
  end

  # Parent/child relationships
  def parent_ranges
    NetworkRange.where("?::inet << network AND masklen(network) < ?", network.to_s, prefix_length)
                 .order("masklen(network) DESC")
  end

  def child_ranges
    NetworkRange.where("network >> ?::inet AND masklen(network) > ?", network.to_s, prefix_length)
                 .order("masklen(network) ASC")
  end

  def sibling_ranges
    NetworkRange.where("masklen(network) = ?", prefix_length)
                 .where("network && ?::inet", network.to_s)
                 .where.not(id: id)
  end

  # Find nearest parent with intelligence data
  def parent_with_intelligence
    # Find all parent ranges (networks that contain this network)
    # and look for any with intelligence data, ordered by specificity
    NetworkRange.where("?::inet <<= network", network.to_s)
                .where("masklen(network) < ?", prefix_length)
                .where("(asn IS NOT NULL OR company IS NOT NULL OR country IS NOT NULL OR is_datacenter = true OR is_vpn = true OR is_proxy = true)")
                .order("masklen(network) DESC")
                .first
  end

  # Check if this network or any parent has IPAPI data
  def has_ipapi_data_available?
    return true if has_network_data_from?(:ipapi)

    parent_ranges.any? { |parent| parent.has_network_data_from?(:ipapi) }
  end

  # Generic API fetching status management
  def is_fetching_api_data?(source)
    fetching_status = network_data&.dig('fetching_status') || {}
    fetching_status[source.to_s] &&
      fetching_status[source.to_s]['started_at'] &&
      fetching_status[source.to_s]['started_at'] > 5.minutes.ago.to_f
  end

  def mark_as_fetching_api_data!(source)
    self.network_data ||= {}
    self.network_data['fetching_status'] ||= {}
    self.network_data['fetching_status'][source.to_s] = {
      'started_at' => Time.current.to_f,
      'job_id' => SecureRandom.hex(8)
    }
    save!
  end

  def clear_fetching_status!(source)
    if network_data&.dig('fetching_status')&.dig(source.to_s)
      self.network_data['fetching_status'].delete(source.to_s)
      # Clean up empty fetching_status hash
      self.network_data.delete('fetching_status') if self.network_data['fetching_status'].empty?
      save!
    end
  end

  # Check if we should fetch API data (not available and not currently being fetched)
  def should_fetch_api_data?(source)
    return false if send("has_network_data_from?(#{source})") if respond_to?("has_network_data_from?(#{source})")
    return false if is_fetching_api_data?(source)

    true
  end

  # Check if this network or any parent has IPAPI data available and no active fetch
  def should_fetch_ipapi_data?
    return false if has_ipapi_data_available?
    return false if is_fetching_api_data?(:ipapi)

    # Also check if any parent is currently fetching IPAPI data
    return false if parent_ranges.any? { |parent| parent.is_fetching_api_data?(:ipapi) }

    true
  end

  def inherited_intelligence
    return own_intelligence if has_intelligence?

    parent = parent_with_intelligence
    parent ? parent.own_intelligence.merge(inherited: true, parent_cidr: parent.cidr) : {}
  end

  def has_intelligence?
    asn.present? || company.present? || country.present? ||
    is_datacenter? || is_proxy? || is_vpn?
  end

  def own_intelligence
    {
      asn: asn,
      asn_org: asn_org,
      company: company,
      country: country,
      is_datacenter: is_datacenter,
      is_proxy: is_proxy,
      is_vpn: is_vpn,
      inherited: false,
      source: source
    }
  end

  def agent_tally
    # Rails.cache.fetch("#{to_s}:agent_tally", expires_in: 5.minutes) do 
      events.map(&:user_agent).tally
    # end
  end

  # Geographic lookup
  def geo_lookup_country!
    return if country.present?

    sample_ip = network_address
    geo_country = GeoIpService.lookup_country(sample_ip)
    update!(country: geo_country) if geo_country.present?
  rescue => e
    Rails.logger.error "Failed to lookup geo location for network range #{cidr}: #{e.message}"
  end

  # Class methods for network operations
  def self.contains_ip(ip_string)
    where("network >>= ?", ip_string)
       .order("masklen(network) DESC") # Most specific first
  end

  def self.overlapping(range_cidr)
    where("network && ?", range_cidr)
  end

  def self.findd(cidr)
    cidr = cidr.gsub("_", "/")
    cidr = "#{cidr}/24" unless cidr.include?("/")
    find_by(network: cidr)
  end

  def self.find_or_create_by_cidr(cidr, user: nil, source: nil, reason: nil)
    find_or_create_by(network: cidr) do |range|
      range.user = user
      range.source = source || 'user_created'
      range.creation_reason = reason
    end
  end

  def self.import_from_cidr(cidr, **attributes)
    find_or_create_by(network: cidr) do |range|
      range.assign_attributes(attributes)
    end
  end

  # Convenience methods for JSON fields
  def abuser_scores_hash
    abuser_scores ? JSON.parse(abuser_scores) : {}
  rescue JSON::ParserError
    {}
  end

  def abuser_scores_hash=(hash)
    self.abuser_scores = hash.to_json
  end

  def additional_data_hash
    additional_data ? JSON.parse(additional_data) : {}
  rescue JSON::ParserError
    {}
  end

  def additional_data_hash=(hash)
    self.additional_data = hash.to_json
  end

  # Network data accessors for different data sources
  # network_data is a JSONB column with namespaced data:
  # {
  #   geolite: {...},   # MaxMind GeoLite2 data
  #   ipapi: {...},     # IPAPI.is enrichment data
  #   abuseipdb: {...}, # Future: AbuseIPDB data
  #   shodan: {...}     # Future: Shodan data
  # }
  def network_data_for(source)
    network_data&.dig(source.to_s) || {}
  end

  def set_network_data(source, data)
    self.network_data ||= {}
    self.network_data[source.to_s] = data
  end

  # Check if we have network data from a specific source
  def has_network_data_from?(source)
    network_data&.key?(source.to_s) && network_data[source.to_s].present?
  end

  # IPAPI tracking at /24 granularity
  # Find or create the /24 network for a given IP address
  def self.find_or_create_tracking_network_for_ip(ip_address)
    ip = IPAddr.new(ip_address.to_s)

    # Create /24 network for IPv4, /64 for IPv6
    tracking_cidr = if ip.ipv4?
      "#{ip.mask(24)}/24"
    else
      "#{ip.mask(64)}/64"
    end

    find_or_create_by(network: tracking_cidr) do |range|
      range.source = 'auto_generated'
      range.creation_reason = 'IPAPI tracking network'
    end
  end

  # Check if we should fetch IPAPI data for a given IP address
  # Uses /24 networks as the tracking unit
  def self.should_fetch_ipapi_for_ip?(ip_address)
    tracking_network = find_or_create_tracking_network_for_ip(ip_address)

    # Check if /24 has been queried recently
    queried_at = tracking_network.network_data&.dig('ipapi_queried_at')
    return true if queried_at.nil?

    # Check if IPAPI returned a CIDR that actually covers this IP
    # (handles edge case where IPAPI returns /25 or more specific)
    returned_cidr = tracking_network.network_data&.dig('ipapi_returned_cidr')
    if returned_cidr.present?
      begin
        returned_range = IPAddr.new(returned_cidr)
        ip = IPAddr.new(ip_address.to_s)
        # If the IP is NOT covered by what IPAPI returned, fetch again
        return true unless returned_range.include?(ip)
      rescue IPAddr::InvalidAddressError => e
        Rails.logger.warn "Invalid CIDR stored in ipapi_returned_cidr: #{returned_cidr}"
      end
    end

    # Re-query after 1 year
    Time.at(queried_at) < 1.year.ago
  rescue => e
    Rails.logger.error "Error checking IPAPI fetch status for #{ip_address}: #{e.message}"
    true # Default to fetching on error
  end

  # Mark that we've queried IPAPI for this /24 network
  # @param returned_cidr [String] The CIDR that IPAPI actually returned (may be more specific than /24)
  def mark_ipapi_queried!(returned_cidr)
    self.network_data ||= {}
    self.network_data['ipapi_queried_at'] = Time.current.to_i
    self.network_data['ipapi_returned_cidr'] = returned_cidr
    save!
  end

  # String representations
  def to_s
    cidr
  end

  def to_param
    cidr.to_s.gsub('/', '_')
  end

  # Analytics methods - events_count is now a counter cache column maintained by database triggers
  # This is much more performant than the previous implementation that did complex network queries
  def events_count
    self[:events_count] || 0
  end

  def events
    Event.where("ip_address <<= ?", cidr)
  end

  def recent_events(limit: 100)
    events.recent.limit(limit)
  end

  def blocking_rules
    rules.where(waf_action: :deny, enabled: true)
  end

  def active_rules
    rules.enabled.where("expires_at IS NULL OR expires_at > ?", Time.current)
  end

  # Find all network ranges that are contained by this network and have enabled rules
  # Used when creating a supernet rule to identify redundant child rules
  def child_network_ranges_with_rules
    NetworkRange
      .where("network << ?::inet", network.to_s)  # network is strictly contained by this network
      .joins(:rules)
      .where(rules: { enabled: true })
      .distinct
  end

  # Find all enabled rules on child network ranges (more specific networks)
  # Used after creating a rule to expire redundant child rules
  def child_rules
    Rule
      .joins(:network_range)
      .where("network_ranges.network << ?::inet", cidr)
      .where(enabled: true)
  end

  # Find all network ranges that contain this network and have enabled rules
  # Used to check if creating a rule would be redundant
  def parent_network_ranges_with_rules
    NetworkRange
      .where("?::inet << network", cidr)  # this network is strictly contained by parent
      .joins(:rules)
      .where(rules: { enabled: true })
      .distinct
  end

  # Find all enabled rules on parent network ranges (less specific networks)
  # Used before creating a rule to check if it would be redundant
  def supernet_rules
    Rule
      .joins(:network_range)
      .where("?::inet << network_ranges.network", cidr)
      .where(enabled: true)
      .order("masklen(network_ranges.network) DESC")  # Most specific supernet first
  end

  # Check if this network range needs WAF policy evaluation
  # Returns true if:
  # - Never been evaluated, OR
  # - Any WafPolicy has been updated since last evaluation
  def needs_policy_evaluation?
    return true if policies_evaluated_at.nil?

    latest_policy_update = WafPolicy.maximum(:updated_at)
    return false if latest_policy_update.nil? # No policies exist

    policies_evaluated_at < latest_policy_update
  end

  private

  def set_default_source
    self.source ||= 'api_imported'
  end

  def should_update_children_inheritance?
    saved_change_to_attribute?(:asn) ||
    saved_change_to_attribute?(:company) ||
    saved_change_to_attribute?(:country) ||
    saved_change_to_attribute?(:is_datacenter) ||
    saved_change_to_attribute?(:is_proxy) ||
    saved_change_to_attribute?(:is_vpn)
  end

  def update_children_inheritance!
    # Find child ranges that don't have their own intelligence
    child_without_intelligence = child_ranges.where(
      asn: nil,
      company: nil,
      country: nil,
      is_datacenter: false,
      is_proxy: false,
      is_vpn: false
    )

    child_without_intelligence.find_each do |child|
      Rails.logger.info "Child range #{child.cidr} can now inherit from parent #{cidr}"
      # The inherited_intelligence method will pick up the new parent data
    end
  end

  # Import-related class methods
  def self.import_stats_by_source
    group(:source)
      .select(:source, 'COUNT(*) as count', 'MIN(created_at) as first_import', 'MAX(updated_at) as last_update')
      .order(:source)
  end

  def self.geolite_coverage_stats
    {
      total_networks: geolite_imported.count,
      asn_networks: geolite_asn.count,
      country_networks: geolite_country.count,
      with_asn_data: geolite_imported.where.not(asn: nil).count,
      with_country_data: geolite_imported.where.not(country: nil).count,
      with_proxy_data: geolite_imported.where(is_proxy: true).count,
      unique_countries: geolite_imported.distinct.count(:country),
      unique_asns: geolite_imported.distinct.count(:asn),
      ipv4_networks: geolite_imported.ipv4.count,
      ipv6_networks: geolite_imported.ipv6.count
    }
  end

  def self.find_by_ip_or_network(query)
    return none if query.blank?

    begin
      # Try to parse as IP address first
      ip = IPAddr.new(query)
      where("network >>= ?", ip.to_s)
    rescue IPAddr::InvalidAddressError
      # Try to parse as network
      begin
        network = IPAddr.new(query)
        where(network: network.to_s)
      rescue IPAddr::InvalidAddressError
        none
      end
    end
  end
end