require "active_support/all"
require_relative "./historiographer/history"
require_relative "./historiographer/postgres_migration"
require_relative "./historiographer/safe"
require_relative "./historiographer/relation"

# Historiographer takes "histories" (think audits or snapshots) of your model whenever you make changes.
#
# Core business data stored in histories can never be changed or destroyed (at least not from Rails-land), offering you
# a little more peace of mind (just a little).
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# A little example:
#
# photo = Photo.create(file: "cool.jpg")
# photo.histories # => [ <#PhotoHistory file: cool.jpg > ]
#
# photo.file = "fun.jpg"
# photo.save!
# photo.histories.reload # => [ <#PhotoHistory file: cool.jpg >, <#PhotoHistory file: fun.jpg> ]
#
# photo.histories.last.destroy! # => false
#
# photo.histories.last.update!(file: "bad.jpg") # => false
#
# photo.histories.last.file = "bad.jpg"
# photo.histories.last.save! # => false
#
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
#
# How to use:
#
# 1) Add historiographer to your apps dependencies folder
#
# Ex: sudo ln -s ../../../shared/historiographer historiographer
#
# 2) Add historiographer to your apps gem file and bundle
#
# gem 'historiographer', path: 'dependencies/historiographer', require: 'historiographer'
#
# 3) Create a primary table
#
# create_table :photos do |t|
#   t.string :file
# end
#
# 4) Create history table. 't.histories' pulls in all the primary tables attributes plus a few required by historiographer.
#
# require "historiographer/postgres_migration"
# class CreatePhotoHistories < ActiveRecord::Migration
#   def change
#     create_table :photo_histories do |t|
#       t.histories
#     end
#   end
# end
#
# 5) Include Historiographer in the primary class:
#
# class Photo < ActiveRecord::Base
#   include Historiographer
# end
#
# 6) Create a history class
#
# class PhotoHistory < ActiveRecord::Base (or whereever your app inherits from. Ex CPG: CpgConnection, Shoppers: ShoppersRecord, etc)
# end
#
# 7) Enjoy!
#
module Historiographer
  extend ActiveSupport::Concern

  class HistoryUserIdMissingError < StandardError; end

  UTC = Time.now.in_time_zone("UTC").time_zone

  included do |base|
    after_save :record_history, if: :should_record_history?
    validate :validate_history_user_id_present, if: :should_validate_history_user_id_present?

    def should_validate_history_user_id_present?
      true
    end

    def validate_history_user_id_present
      if @no_history.nil? && (!history_user_id.present? || !history_user_id.is_a?(Integer))
        errors.add(:history_user_id, "must be an integer")
      end
    end

    alias_method :destroy_without_history, :destroy

    def destroy_with_history(history_user_id: nil)
      history_user_absent_action if history_user_id.nil?

      current_history = histories.where(history_ended_at: nil).order("id desc").limit(1).last
      current_history.update!(history_ended_at: UTC.now) if current_history.present?

      if respond_to?(:paranoia_destroy)
        self.history_user_id = history_user_id
        paranoia_destroy
      else
        @no_history = true
        destroy_without_history
        @no_history = false
      end
    end

    alias_method :destroy, :destroy_with_history

    def assign_attributes(new_attributes)
      huid = new_attributes[:history_user_id]

      if huid.present?
        self.class.nested_attributes_options.each do |association, _|
          reflection = self.class.reflect_on_association(association)
          assoc_attrs = new_attributes["#{association}_attributes"]

          if assoc_attrs.present?
            if reflection.collection?
              assoc_attrs.values.each do |hash|
                hash.merge!(history_user_id: huid)
              end
            else
              assoc_attrs.merge!(history_user_id: huid)
            end
          end
        end
      end

      super
    end

    def historiographer_changes?
      case Rails.version.to_f
      when 0..5 then changed? && valid?
      when 5.1..7 then saved_changes?
      else
        raise "Unsupported Rails version"
      end
    end

    #
    # If there are any changes, and the model is valid,
    # and we're not force-overriding history recording,
    # then record history after successful save.
    #
    def should_record_history?
      historiographer_changes? && !@no_history
    end

    attr_accessor :history_user_id

    class_name = "#{base.name}History"

    begin
      class_name.constantize
    rescue
      history_class_initializer = Class.new(ActiveRecord::Base) do
      end

      Object.const_set(class_name, history_class_initializer)
    end

    klass = class_name.constantize

    if base.respond_to?(:histories)
      raise "#{base} already has histories. Talk to Brett if this is a legit use case."
    else
      opts = { class_name: class_name }
      opts.merge!(foreign_key: klass.history_foreign_key) if klass.respond_to?(:history_foreign_key)
      has_many :histories, opts
      has_one :current_history, -> { current }, opts
    end

    klass.send(:include, Historiographer::History) unless klass.ancestors.include?(Historiographer::History)

    #
    # The acts_as_paranoid gem, which we tend to use with our History classes,
    # uses update_columns to update deleted_at fields.
    #
    # In order to make sure these changes are persisted into Histories objects,
    # we also have to record history here.
    #
    module UpdateColumnsWithHistory
      def update_columns(*args)
        opts = args.extract_options!
        any_changes = opts.keys.reject { |k| k == "id" }.any?

        transaction do
          persisted = super(opts)

          if any_changes && persisted
            record_history
          end
        end
      end
    end

    base.send(:prepend, UpdateColumnsWithHistory)

    def save_without_history(*args, &block)
      @no_history = true
      save(*args, &block)
      @no_history = false
    end

    def save_without_history!(*args, &block)
      @no_history = true
      save!(*args, &block)
      @no_history = false
    end

    private

    def history_user_absent_action
      raise HistoryUserIdMissingError.new("history_user_id must be passed in order to save record with histories! If you are in a context with no history_user_id, explicitly call #save_without_user")
    end

    #
    # Save a record of the most recent changes, with the current
    # time as history_started_at, and the provided user as history_user_id.
    #
    # Find the most recent history, and update its history_ended_at timestamp
    #
    def record_history
      history_user_absent_action if history_user_id.nil?

      attrs = attributes.clone
      history_class = self.class.history_class
      foreign_key = history_class.history_foreign_key

      now = UTC.now
      attrs.merge!(foreign_key => attrs["id"], history_started_at: now, history_user_id: history_user_id)

      attrs = attrs.except("id")

      current_history = histories.where(history_ended_at: nil).order("id desc").limit(1).last

      unless foreign_key.present? && history_class.present?
        raise "Need foreign key and history class to save history!"
      else
        history_class.create!(attrs)
        current_history.update!(history_ended_at: now) if current_history.present?
      end
    end
  end

  class_methods do

    #
    # E.g. SponsoredProductCampaign => SponsoredProductCampaignHistory
    #
    def history_class
      "#{name}History".constantize
    end

    def relation
      super.tap { |r| r.extend Historiographer::Relation }
    end
  end
end