# frozen_string_literal: true

require "active_record"
require "active_support/core_ext/time/calculations"
require "activerecord-bitemporal/bitemporal"
require "activerecord-bitemporal/scope"
require "activerecord-bitemporal/patches"
require "activerecord-bitemporal/version"
require "activerecord-bitemporal/visualizer"

module ActiveRecord::Bitemporal
  DEFAULT_VALID_FROM = Time.utc(1900, 12, 31).in_time_zone.freeze
  DEFAULT_VALID_TO   = Time.utc(9999, 12, 31).in_time_zone.freeze
  DEFAULT_TRANSACTION_FROM = Time.utc(1900, 12, 31).in_time_zone.freeze
  DEFAULT_TRANSACTION_TO   = Time.utc(9999, 12, 31).in_time_zone.freeze

  extend ActiveSupport::Concern
  included do
    bitemporalize
  end
end

module ActiveRecord::Bitemporal::Bitemporalize
  using Module.new {
    refine ::ActiveRecord::Base do
      class << ::ActiveRecord::Base
        def prepend_relation_delegate_class(mod)
          relation_delegate_class(ActiveRecord::Relation).prepend mod
          relation_delegate_class(ActiveRecord::AssociationRelation).prepend mod
          relation_delegate_class(ActiveRecord::Associations::CollectionProxy).prepend mod
        end
      end
    end
  }

  module ClassMethods
    include ActiveRecord::Bitemporal::Relation::Finder

    DEFAULT_ATTRIBUTES = {
      valid_from:       ActiveRecord::Bitemporal::DEFAULT_VALID_FROM,
      valid_to:         ActiveRecord::Bitemporal::DEFAULT_VALID_TO,
      transaction_from: ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_FROM,
      transaction_to:   ActiveRecord::Bitemporal::DEFAULT_TRANSACTION_TO
    }.freeze

    def bitemporal_id_key
      'bitemporal_id'
    end

    # Override ActiveRecord::Core::ClassMethods#cached_find_by_statement
    # `.find_by` not use caching
    def cached_find_by_statement(key, &block)
      ActiveRecord::StatementCache.create(connection, &block)
    end

    def inherited(klass)
      super
      klass.prepend_relation_delegate_class ActiveRecord::Bitemporal::Relation
      if relation_delegate_class(ActiveRecord::Relation).ancestors.include? ActiveRecord::Bitemporal::Relation::MergeWithExceptBitemporalDefaultScope
        klass.relation_delegate_class(ActiveRecord::Relation).prepend ActiveRecord::Bitemporal::Relation::MergeWithExceptBitemporalDefaultScope
      end
    end

  private
    def load_schema!
      super

      DEFAULT_ATTRIBUTES.each do |name, default_value|
        type = type_for_attribute(name)
        define_attribute(name.to_s, type, default: default_value)
      end
    end
  end

  module InstanceMethods
    include ActiveRecord::Bitemporal::Persistence

    def swap_id!(without_clear_changes_information: false)
      @_swapped_id = self.id
      self.id = self.send(bitemporal_id_key)
      clear_attribute_changes([:id]) unless without_clear_changes_information
    end

    def swapped_id
      @_swapped_id || self.id
    end

    def bitemporal_id_key
      self.class.bitemporal_id_key
    end

    def bitemporal_ignore_update_columns
      []
    end

    def id_in_database
      swapped_id.presence || super
    end

    def valid_from_cannot_be_greater_equal_than_valid_to
      if valid_from && valid_to && valid_from >= valid_to
        errors.add(:valid_from, "can't be greater equal than valid_to")
      end
    end

    def transaction_from_cannot_be_greater_equal_than_transaction_to
      if transaction_from && transaction_to && transaction_from >= transaction_to
        errors.add(:transaction_from, "can't be greater equal than transaction_to")
      end
    end
  end

  def bitemporalize(
    enable_strict_by_validates_bitemporal_id: false,
    enable_default_scope: true,
    enable_merge_with_except_bitemporal_default_scope: false
  )
    extend ClassMethods
    include InstanceMethods
    include ActiveRecord::Bitemporal::Scope

    if enable_merge_with_except_bitemporal_default_scope
      relation_delegate_class(ActiveRecord::Relation).prepend ActiveRecord::Bitemporal::Relation::MergeWithExceptBitemporalDefaultScope
    end

    if enable_default_scope
      default_scope {
        bitemporal_default_scope
      }
    end

    after_create do
      # MEMO: #update_columns is not call #_update_row (and validations, callbacks)
      update_columns(bitemporal_id_key => swapped_id) unless send(bitemporal_id_key)
      swap_id!(without_clear_changes_information: true)
    end

    after_find do
      self.swap_id! if self.send(bitemporal_id_key).present?
    end

    # Callback hook to `validates :xxx, uniqueness: true`
    const_set(:UniquenessValidator, Class.new(ActiveRecord::Validations::UniquenessValidator) {
      prepend ActiveRecord::Bitemporal::Uniqueness
    })

    # validations
    validates :valid_from, presence: true
    validates :valid_to, presence: true
    validates :transaction_from, presence: true
    validates :transaction_to, presence: true
    validate :valid_from_cannot_be_greater_equal_than_valid_to
    validate :transaction_from_cannot_be_greater_equal_than_transaction_to

    validates bitemporal_id_key, uniqueness: true, allow_nil: true, strict: enable_strict_by_validates_bitemporal_id

    prepend_relation_delegate_class ActiveRecord::Bitemporal::Relation
  end
end

ActiveSupport.on_load(:active_record) do
  ActiveRecord::Base
    .extend ActiveRecord::Bitemporal::Bitemporalize

  ActiveRecord::Base
    .prepend ActiveRecord::Bitemporal::Patches::Persistence

  ActiveRecord::Relation::Merger
    .prepend ActiveRecord::Bitemporal::Patches::Merger

  ActiveRecord::Associations::Association
    .prepend ActiveRecord::Bitemporal::Patches::Association

  ActiveRecord::Associations::ThroughAssociation
    .prepend ActiveRecord::Bitemporal::Patches::ThroughAssociation

  ActiveRecord::Associations::SingularAssociation
    .prepend ActiveRecord::Bitemporal::Patches::SingularAssociation

  ActiveRecord::Reflection::AssociationReflection
    .prepend ActiveRecord::Bitemporal::Patches::AssociationReflection
end