# frozen_string_literal: true

# Rule - WAF rule management with NetworkRange integration
#
# Rules define actions to take for matching traffic conditions.
# Network rules are associated with NetworkRange objects for rich context.
class Rule < ApplicationRecord
  # Rule enums (prefix needed to avoid rate_limit collision)
  # Canonical WAF action order - aligned with Agent and Event models
  enum :waf_action, { deny: 0, allow: 1, redirect: 2, challenge: 3, log: 4 }, prefix: :action
  enum :waf_rule_type, { network: 0, rate_limit: 1, path_pattern: 2 }, prefix: :type

  SOURCES = %w[manual auto:scanner_detected auto:rate_limit_exceeded auto:bot_detected imported default manual:surgical_block manual:surgical_exception policy].freeze

  # Associations
  belongs_to :user
  belongs_to :network_range, optional: true
  belongs_to :waf_policy, optional: true
  has_many :events, dependent: :nullify

  # Validations
  validates :waf_rule_type, presence: true, inclusion: { in: waf_rule_types.keys }
  validates :waf_action, presence: true, inclusion: { in: waf_actions.keys }
  validates :conditions, presence: true, unless: :network_rule?
  validates :enabled, inclusion: { in: [true, false] }
  validates :source, inclusion: { in: SOURCES }

  # Custom validations
  validate :validate_conditions_by_type
  validate :validate_metadata_by_action
  validate :network_range_required_for_network_rules
  validate :validate_network_consistency, if: :network_rule?
  validate :no_supernet_rule_exists, if: :should_check_supernet?

  # Scopes
  scope :enabled, -> { where(enabled: true) }
  scope :disabled, -> { where(enabled: false) }
  scope :active, -> { enabled.where("expires_at IS NULL OR expires_at > ?", Time.current) }
  scope :expired, -> { where("expires_at IS NOT NULL AND expires_at <= ?", Time.current) }
  scope :by_type, ->(type) { where(waf_rule_type: type) }
  scope :network_rules, -> { where(waf_rule_type: :network) }
  scope :rate_limit_rules, -> { where(waf_rule_type: :rate_limit) }
  scope :path_pattern_rules, -> { where(waf_rule_type: :path_pattern) }
  scope :by_source, ->(source) { where(source: source) }
  scope :surgical_blocks, -> { where(source: "manual:surgical_block") }
  scope :surgical_exceptions, -> { where(source: "manual:surgical_exception") }
  scope :policy_generated, -> { where(source: "policy") }
  scope :from_waf_policy, ->(waf_policy) { where(waf_policy: waf_policy) }

  # Action scopes (manual to avoid enum collision with rate_limit)
  scope :deny, -> { where(waf_action: :deny) }
  scope :allow, -> { where(waf_action: :allow) }

  # Sync queries
  scope :since, ->(timestamp) { where("updated_at >= ?", Time.at(timestamp)).order(:updated_at, :id) }
  scope :sync_order, -> { order(:updated_at, :id) }

  # Callbacks
  before_validation :set_defaults
  before_validation :parse_json_fields
  before_save :calculate_priority_for_network_rules
  after_create :expire_redundant_child_rules, if: :should_expire_child_rules?

  # Rule type checks
  def network_rule?
    type_network?
  end

  def rate_limit_rule?
    type_rate_limit?
  end

  def path_pattern_rule?
    type_path_pattern?
  end

  # Network-specific methods
  def network_range?
    network_range.present?
  end

  def cidr
    network_rule? ? network_range&.cidr : conditions&.dig("cidr")
  end

  def prefix_length
    network_rule? ? network_range&.prefix_length : cidr&.split("/")&.last&.to_i
  end

  def network_intelligence
    return {} unless network_rule? && network_range

    network_range.inherited_intelligence
  end

  def network_address
    network_rule? ? network_range&.network_address : nil
  end

  # Surgical block methods
  def surgical_block?
    source == "manual:surgical_block"
  end

  def surgical_exception?
    source == "manual:surgical_exception"
  end

  # Policy-generated rule methods
  def policy_generated?
    source == "policy"
  end

  # Action-specific methods
  def redirect_action?
    action_redirect?
  end

  def challenge_action?
    action_challenge?
  end

  # Redirect/challenge convenience methods
  def redirect_url
    metadata_hash['redirect_url']
  end

  def redirect_status
    metadata&.dig('redirect_status') || 302
  end

  def challenge_type
    metadata&.dig('challenge_type') || 'captcha'
  end

  def challenge_message
    metadata&.dig('challenge_message')
  end

  def related_surgical_rules
    if surgical_block?
      # Find the corresponding exception rule
      surgical_exceptions.where(
        conditions: { cidr: network_address ? "#{network_address}/32" : nil }
      )
    elsif surgical_exception?
      # Find the parent block rule
      surgical_blocks.joins(:network_range).where(
        network_ranges: { network: parent_cidr }
      )
    else
      Rule.none
    end
  end

  # Rule lifecycle
  def active?
    enabled? && !expired?
  end

  def expired?
    expires_at.present? && expires_at <= Time.current
  end

  def activate!
    update!(enabled: true)
  end

  def deactivate!
    update!(enabled: false)
  end

  def disable!(reason: nil)
    new_metadata = metadata_hash.merge(
      disabled_at: Time.current.iso8601,
      disabled_reason: reason
    )
    update!(
      enabled: false,
      metadata: new_metadata
    )
  end

  def extend_expiry!(duration)
    new_expiry = Time.current + duration
    update!(expires_at: new_expiry)
  end

  # Agent serialization
  def to_agent_format
    format = {
      id: id,
      waf_rule_type: waf_rule_type,
      waf_action: waf_action,  # Use the enum field directly
      conditions: agent_conditions,
      priority: agent_priority,
      expires_at: expires_at&.to_i,  # Agents expect Unix timestamps
      enabled: enabled,
      source: source,
      metadata: metadata || {},
      created_at: created_at.to_i,   # Agents expect Unix timestamps
      updated_at: updated_at.to_i     # Agents expect Unix timestamps
    }

    # For network rules, resolve the network range to actual IP data
    if network_rule? && network_range
      begin
        ip_range = IPAddr.new(network_range.cidr)
        range = ip_range.to_range

        if ip_range.ipv4?
          format[:network_start] = range.first.to_i
          format[:network_end] = range.last.to_i
        else
          # IPv6 - use binary representation
          format[:network_start] = range.first.hton
          format[:network_end] = range.last.hton
        end

        format[:network_prefix] = network_range.prefix_length
        format[:network_intelligence] = network_intelligence
      rescue => e
        Rails.logger.error "Failed to resolve network range #{network_range.cidr}: #{e.message}"
        # Fallback to CIDR format
        format[:conditions] = { cidr: network_range.cidr }
      end
    end

    # For path_pattern rules, include segment IDs and match type
    if path_pattern_rule?
      format[:conditions] = {
        segment_ids: path_segment_ids,
        match_type: path_match_type
      }
    end

    format
  end

  # Path pattern rule helper methods
  def path_segment_ids
    conditions&.dig("segment_ids") || []
  end

  def path_match_type
    conditions&.dig("match_type")
  end

  def path_segments_text
    return [] if path_segment_ids.empty?
    PathSegment.where(id: path_segment_ids).order(:id).pluck(:segment)
  end

  def path_pattern_display
    return nil unless path_pattern_rule?
    "/" + path_segments_text.join("/")
  end

  # Class methods for rule creation
  def self.create_network_rule(cidr, action: 'deny', user: nil, **options)
    network_range = NetworkRange.find_or_create_by_cidr(cidr, user: user, source: 'user_created')

    create!(
      waf_rule_type: 'network',
      waf_action: action,
      network_range: network_range,
      user: user,
      **options
    )
  end

  def self.create_path_pattern_rule(pattern:, match_type:, action: 'deny', user: nil, **options)
    # Parse pattern string to segments (case-insensitive)
    segments = pattern.split('/').reject(&:blank?).map(&:downcase)

    if segments.empty?
      raise ArgumentError, "Pattern must contain at least one path segment"
    end

    unless %w[exact prefix suffix contains].include?(match_type)
      raise ArgumentError, "Match type must be one of: exact, prefix, suffix, contains"
    end

    # Find or create PathSegment entries
    segment_ids = segments.map do |seg|
      PathSegment.find_or_create_segment(seg).id
    end

    create!(
      waf_rule_type: 'path_pattern',
      waf_action: action,
      conditions: {
        segment_ids: segment_ids,
        match_type: match_type,
        original_pattern: pattern
      },
      metadata: {
        segments: segments,
        pattern_display: "/" + segments.join("/")
      },
      user: user,
      source: options[:source] || 'manual',
      priority: options[:priority] || 50,
      **options.except(:source, :priority)
    )
  end

  def self.create_surgical_block(ip_address, parent_cidr, user: nil, reason: nil, **options)
    # Create block rule for parent range
    network_range = NetworkRange.find_or_create_by_cidr(parent_cidr, user: user, source: 'user_created')

    block_rule = create!(
      waf_rule_type: 'network',
      waf_action: 'deny',
      network_range: network_range,
      source: 'manual:surgical_block',
      user: user,
      metadata: {
        reason: reason,
        surgical_block: true,
        original_ip: ip_address,
        **options[:metadata]
      },
      **options.except(:metadata)
    )

    # Create exception rule for specific IP
    ip_network_range = NetworkRange.find_or_create_by_cidr("#{ip_address}/#{ip_address.include?(':') ? '128' : '32'}", user: user, source: 'user_created')

    exception_rule = create!(
      waf_rule_type: 'network',
      waf_action: 'allow',
      network_range: ip_network_range,
      source: 'manual:surgical_exception',
      user: user,
      priority: ip_network_range.prefix_length, # Higher priority = more specific
      metadata: {
        reason: "Exception for #{ip_address} in surgical block of #{parent_cidr}",
        surgical_exception: true,
        parent_rule_id: block_rule.id,
        **options[:metadata]
      },
      **options.except(:metadata)
    )

    [block_rule, exception_rule]
  end

  def self.create_rate_limit_rule(cidr, limit:, window:, user: nil, action: 'deny', **options)
    network_range = NetworkRange.find_or_create_by_cidr(cidr, user: user, source: 'user_created')

    create!(
      waf_rule_type: 'rate_limit',
      waf_action: action,  # Action to take when rate limit exceeded (deny, redirect, challenge, log)
      network_range: network_range,
      conditions: { cidr: cidr, scope: 'ip' },
      metadata: {
        limit: limit,
        window: window,
        **options[:metadata]
      },
      user: user,
      **options.except(:metadata)
    )
  end

  # Sync and versioning
  def self.latest_version
    max_time = maximum(:updated_at)
    max_time ? max_time.to_i : Time.current.to_i
  end

  def self.active_for_agent
    active.sync_order.map(&:to_agent_format)
  end

  # Analytics methods
  def matching_events(limit: 100)
    return Event.none unless network_rule? && network_range

    # This would need efficient IP range queries
    # For now, simple IP match
    Event.where("ip_address <<= ?", network_range.cidr)
         .recent
         .limit(limit)
  end

  def effectiveness_stats
    return {} unless network_rule?

    events = matching_events
    {
      total_events: events.count,
      blocked_events: events.blocked.count,
      allowed_events: events.allowed.count,
      block_rate: events.count > 0 ? (events.blocked.count.to_f / events.count * 100).round(2) : 0
    }
  end

  # Helper method to safely access metadata as hash
  def metadata_hash
    case metadata
    when Hash
      metadata
    when String
      metadata.present? ? (JSON.parse(metadata) rescue {}) : {}
    else
      {}
    end
  end

  private

  def set_defaults
    self.enabled = true if enabled.nil?
    self.conditions ||= {}
    self.metadata ||= {}
    self.source ||= "manual"

    # Set system user for auto-generated rules if no user is set
    if source&.start_with?('auto:') || source == 'default'
      self.user ||= User.find_by(role: 1) # admin role
    end
  end

  def calculate_priority_for_network_rules
    if network_rule? && network_range
      self.priority = network_range.prefix_length
    end
  end

  def agent_conditions
    if network_rule?
      { cidr: cidr }
    else
      conditions || {}
    end
  end

  def agent_priority
    if network_rule?
      prefix_length || 0
    else
      priority || 0
    end
  end

  def validate_conditions_by_type
    case waf_rule_type
    when "network"
      # Network rules don't need conditions in DB - stored in network_range
      true
    when "rate_limit"
      validate_rate_limit_conditions
    when "path_pattern"
      validate_path_pattern_conditions
    end
  end

  def validate_rate_limit_conditions
    scope = conditions&.dig("scope")
    cidr_value = conditions&.dig("cidr")

    if scope.blank?
      errors.add(:conditions, "must include 'scope' for rate_limit rules")
    end

    unless metadata&.dig("limit").present? && metadata&.dig("window").present?
      errors.add(:metadata, "must include 'limit' and 'window' for rate_limit rules")
    end
  end

  def validate_path_pattern_conditions
    segment_ids = conditions&.dig("segment_ids")
    match_type = conditions&.dig("match_type")

    if segment_ids.blank? || !segment_ids.is_a?(Array)
      errors.add(:conditions, "must include 'segment_ids' array for path_pattern rules")
    end

    unless %w[exact prefix suffix contains].include?(match_type)
      errors.add(:conditions, "match_type must be one of: exact, prefix, suffix, contains")
    end

    # Validate that all segment IDs exist
    if segment_ids.is_a?(Array) && segment_ids.any?
      existing_ids = PathSegment.where(id: segment_ids).pluck(:id)
      missing_ids = segment_ids - existing_ids
      if missing_ids.any?
        errors.add(:conditions, "references non-existent path segment IDs: #{missing_ids.join(', ')}")
      end
    end
  end

  def validate_metadata_by_action
    case waf_action
    when "redirect"
      unless metadata&.dig("redirect_url").present?
        errors.add(:metadata, "must include 'redirect_url' for redirect action")
      end
    when "challenge"
      # Challenge is flexible - can use defaults
      challenge_type_value = metadata&.dig("challenge_type")
      if challenge_type_value && !%w[captcha javascript proof_of_work].include?(challenge_type_value)
        errors.add(:metadata, "challenge_type must be one of: captcha, javascript, proof_of_work")
      end
    end
  end

  def network_range_required_for_network_rules
    if network_rule? && network_range.nil?
      errors.add(:network_range, "is required for network rules")
    end
  end

  def validate_network_consistency
    return unless network_rule? && network_range

    # For network rules, we don't use conditions - the network_range handles everything
    # So we can skip this validation for now
    true
  end

  def parent_cidr
    return nil unless network_range

    # Find a broader network range that contains this one
    network_range.parent_ranges.first&.cidr
  end

  def parse_json_fields
    # Parse conditions if it's a string
    if conditions.is_a?(String) && conditions.present?
      begin
        self.conditions = JSON.parse(conditions) if conditions != "{}"
      rescue JSON::ParserError
        self.conditions = {}
      end
    end

    # Parse metadata if it's a string
    if metadata.is_a?(String) && metadata.present?
      begin
        self.metadata = JSON.parse(metadata) if metadata != "{}"
      rescue JSON::ParserError
        self.metadata = {}
      end
    end

    # Ensure they are hashes
    self.conditions ||= {}
    self.metadata ||= {}
  end

  # Supernet/subnet redundancy checking
  def should_check_supernet?
    network_rule? && network_range.present? && new_record?
  end

  def no_supernet_rule_exists
    return unless network_range

    supernet_rule = network_range.supernet_rules.first
    if supernet_rule
      errors.add(
        :base,
        "A supernet rule already covers this network. " \
        "Rule ##{supernet_rule.id} for #{supernet_rule.network_range.cidr} " \
        "(action: #{supernet_rule.waf_action}) makes this rule redundant."
      )
    end
  end

  def should_expire_child_rules?
    network_rule? && network_range.present? && enabled?
  end

  def expire_redundant_child_rules
    return unless network_range

    child_rules = network_range.child_rules
    return if child_rules.empty?

    expired_count = 0
    child_rules.find_each do |child_rule|
      # Disable the child rule and mark it as redundant
      child_rule.update!(
        enabled: false,
        metadata: child_rule.metadata_hash.merge(
          disabled_at: Time.current.iso8601,
          disabled_reason: "Redundant - covered by supernet rule ##{id} (#{network_range.cidr})",
          superseded_by_rule_id: id
        )
      )
      expired_count += 1
    end

    if expired_count > 0
      Rails.logger.info "Rule ##{id}: Expired #{expired_count} redundant child rule(s) for #{network_range.cidr}"
    end
  end

end