# frozen_string_literal: false

class Enterprise < ApplicationRecord
  SELLS = %w(unspecified none own any).freeze
  ENTERPRISE_SEARCH_RADIUS = 100
  # The next Rails version will have named variants but we need to store them
  # ourselves for now.
  LOGO_SIZES = [:thumb, :small, :medium].freeze
  PROMO_IMAGE_SIZES = [:thumb, :medium, :large].freeze
  WHITE_LABEL_LOGO_SIZES = [:default, :mobile].freeze
  VALID_INSTAGRAM_REGEX = %r{\A[a-zA-Z0-9._]{1,30}([^/-]*)\z}

  searchable_attributes :sells, :is_primary_producer, :name
  searchable_associations :properties
  searchable_scopes :is_primary_producer, :is_distributor, :is_hub, :activated, :visible,
                    :ready_for_checkout, :not_ready_for_checkout

  preference :shopfront_message, :text, default: ""
  preference :shopfront_closed_message, :text, default: ""
  preference :shopfront_taxon_order, :string, default: ""
  preference :shopfront_producer_order, :string, default: ""
  preference :shopfront_order_cycle_order, :string, default: "orders_close_at"
  preference :shopfront_product_sorting_method, :string, default: "by_category"
  preference :invoice_order_by_supplier, :boolean, default: false
  preference :product_low_stock_display, :boolean, default: false

  # Allow hubs to restrict visible variants to only those in their inventory
  preference :product_selection_from_inventory_only, :boolean, default: false

  has_paper_trail only: [:owner_id, :sells], on: [:update]

  has_many :relationships_as_parent, class_name: 'EnterpriseRelationship',
                                     foreign_key: 'parent_id',
                                     inverse_of: :parent,
                                     dependent: :destroy
  has_many :relationships_as_child, class_name: 'EnterpriseRelationship',
                                    foreign_key: 'child_id',
                                    inverse_of: :child,
                                    dependent: :destroy
  has_and_belongs_to_many :groups, join_table: 'enterprise_groups_enterprises',
                                   class_name: 'EnterpriseGroup'
  has_many :producer_properties, foreign_key: 'producer_id',
                                 inverse_of: :producer,
                                 dependent: :destroy
  has_many :properties, through: :producer_properties
  has_many :supplied_variants,
           class_name: 'Spree::Variant', foreign_key: 'supplier_id',
           inverse_of: :supplier, dependent: :destroy
  has_many :supplied_products, through: :supplied_variants, source: :product
  has_many :distributed_orders, class_name: 'Spree::Order',
                                foreign_key: 'distributor_id',
                                inverse_of: :distributor,
                                dependent: :restrict_with_error

  belongs_to :address, class_name: 'Spree::Address'
  belongs_to :business_address, optional: true, class_name: 'Spree::Address', dependent: :destroy
  has_many :enterprise_fees, dependent: :restrict_with_error
  has_many :enterprise_roles, dependent: :destroy
  has_many :users, through: :enterprise_roles
  belongs_to :owner, class_name: 'Spree::User',
                     inverse_of: :owned_enterprises
  has_many :distributor_payment_methods,
           inverse_of: :distributor,
           foreign_key: :distributor_id,
           dependent: :restrict_with_error
  has_many :distributor_shipping_methods,
           inverse_of: :distributor,
           foreign_key: :distributor_id,
           dependent: :restrict_with_error
  has_many :payment_methods, through: :distributor_payment_methods
  has_many :shipping_methods, through: :distributor_shipping_methods
  has_many :customers, dependent: :destroy
  has_many :inventory_items, dependent: :destroy
  has_many :tag_rules, dependent: :destroy
  has_one :stripe_account, dependent: :destroy
  has_many :vouchers, dependent: :restrict_with_error
  has_many :connected_apps, dependent: :destroy
  has_many :dfc_permissions, dependent: :destroy
  has_one :custom_tab, dependent: :destroy
  has_one :semantic_link, as: :subject, dependent: :delete

  delegate :latitude, :longitude, :city, :state_name, to: :address

  accepts_nested_attributes_for :address
  accepts_nested_attributes_for :business_address, reject_if: :business_address_empty?,
                                                   allow_destroy: true
  accepts_nested_attributes_for :producer_properties, allow_destroy: true,
                                                      reject_if: lambda { |pp|
                                                        pp[:property_name].blank?
                                                      }
  accepts_nested_attributes_for :tag_rules, allow_destroy: true,
                                            reject_if: lambda { |tag_rule|
                                              tag_rule[:preferred_customer_tags].blank?
                                            }
  accepts_nested_attributes_for :custom_tab

  has_one_attached :terms_and_conditions
  has_one_attached :logo, service: image_service do |attachment|
    attachment.variant :thumb, resize_to_fill: [100, 100], crop: [0, 0, 100, 100]
    attachment.variant :small, resize_to_fill: [180, 180], crop: [0, 0, 180, 180]
    attachment.variant :medium, resize_to_fill: [300, 300], crop: [0, 0, 300, 300]
  end
  has_one_attached :promo_image, service: image_service do |attachment|
    attachment.variant :thumb, resize_to_limit: [100, 100]
    attachment.variant :medium, resize_to_fill: [720, 156]
    attachment.variant :large, resize_to_fill: [1200, 260]
  end
  has_one_attached :white_label_logo, service: image_service do |attachment|
    attachment.variant :default, resize_to_fill: [217, 44]
    attachment.variant :mobile,  resize_to_fill: [128, 26]
  end

  validates :logo,
            processable_file: true,
            content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
  validates :promo_image,
            processable_file: true,
            content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
  validates :white_label_logo,
            processable_file: true,
            content_type: ::Spree::Image::ACCEPTED_CONTENT_TYPES
  validates :terms_and_conditions, content_type: {
    in: "application/pdf",
    message: I18n.t(:enterprise_terms_and_conditions_type_error),
  }

  validates :name, presence: true
  validate :name_is_unique
  validates :sells, presence: true, inclusion: { in: SELLS }
  validates :address, associated: true
  validates :permalink, uniqueness: true, presence: true
  validate :shopfront_taxons
  validate :shopfront_producers
  validate :enforce_ownership_limit, if: lambda { owner_id_changed? && !owner_id.nil? }
  validates :instagram,
            format: {
              with: VALID_INSTAGRAM_REGEX,
              message: Spree.t('errors.messages.invalid_instagram_url')
            }, allow_blank: true
  validate :validate_white_label_logo_link
  validates :external_billing_id,
            format: { with: /\A\S+\z/ },
            allow_blank: true

  before_validation :initialize_permalink, if: lambda { permalink.nil? }
  before_validation :set_unused_address_fields
  after_validation :ensure_owner_is_manager, if: lambda { owner_id_changed? && !owner_id.nil? }

  after_create :set_default_contact
  after_create :relate_to_owners_enterprises

  after_rollback :restore_permalink
  after_touch :touch_distributors
  after_create_commit :send_welcome_email

  scope :by_name, -> { order('name') }
  scope :visible, -> { where(visible: "public") }
  scope :not_hidden, -> { where.not(visible: "hidden") }
  scope :activated, -> { where("sells != 'unspecified'") }
  scope :ready_for_checkout, lambda {
    joins(:shipping_methods).
      joins(:payment_methods).
      merge(Spree::PaymentMethod.available).
      merge(Spree::ShippingMethod.frontend).
      select('DISTINCT enterprises.*')
  }
  scope :not_ready_for_checkout, lambda {
    # When ready_for_checkout is empty, return all rows when there are no enterprises ready for
    # checkout.
    ready_enterprises = Enterprise.default_scoped.ready_for_checkout.
      except(:select).
      select('DISTINCT enterprises.id')

    if ready_enterprises.any?
      where.not(enterprises: { id: ready_enterprises })
    else
      where(nil)
    end
  }
  scope :is_primary_producer, -> { where("enterprises.is_primary_producer IS TRUE") }
  scope :is_distributor, -> { where.not(sells: 'none') }
  scope :is_hub, -> { where(sells: 'any') }
  scope :supplying_variant_in, lambda { |variants|
    joins(:supplied_variants).
      where(spree_variants: { id: variants }).
      select('DISTINCT enterprises.*')
  }

  scope :with_order_cycles_as_supplier_outer, -> {
    joins("
      LEFT OUTER JOIN exchanges
        ON (exchanges.sender_id = enterprises.id AND exchanges.incoming = 't')").
      joins("LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)")
  }

  scope :with_order_cycles_as_distributor_outer, -> {
    joins("
      LEFT OUTER JOIN exchanges
        ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = 'f')").
      joins("LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)")
  }

  scope :with_order_cycles_outer, -> {
    joins("
      LEFT OUTER JOIN exchanges
        ON (exchanges.receiver_id = enterprises.id OR exchanges.sender_id = enterprises.id)").
      joins("LEFT OUTER JOIN order_cycles ON (order_cycles.id = exchanges.order_cycle_id)")
  }

  scope :with_order_cycles_and_exchange_variants_outer, -> {
    with_order_cycles_as_distributor_outer.
      joins("LEFT OUTER JOIN exchange_variants ON (exchange_variants.exchange_id = exchanges.id)").
      joins("LEFT OUTER JOIN spree_variants ON (spree_variants.id = exchange_variants.variant_id)")
  }

  scope :distributors_with_active_order_cycles, lambda {
    with_order_cycles_as_distributor_outer.
      merge(OrderCycle.active).
      select('DISTINCT enterprises.*')
  }

  scope :distributing_variants, lambda { |variants_ids|
    exchanges = joins("
        INNER JOIN exchanges
          ON (exchanges.receiver_id = enterprises.id AND exchanges.incoming = false)
      ").
      joins('INNER JOIN exchange_variants ON (exchange_variants.exchange_id = exchanges.id)').
      joins('INNER JOIN spree_variants ON (spree_variants.id = exchange_variants.variant_id)').
      where(spree_variants: { id: variants_ids }).select('DISTINCT enterprises.id')

    where(id: exchanges)
  }

  scope :managed_by, lambda { |user|
    if user.admin?
      where(nil)
    else
      joins(:enterprise_roles).where(enterprise_roles: { user_id: user.id })
    end
  }

  scope :parents_of_one_union_others, lambda { |one, others|
    where("
      enterprises.id IN
        (SELECT parent_id FROM enterprise_relationships WHERE enterprise_relationships.child_id=?)
      OR enterprises.id IN
        (?)
      ", one, others)
  }

  def business_address_empty?(attributes)
    attributes_exists = attributes['id'].present?
    attributes_empty = attributes.slice(:company, :address1, :city, :phone,
                                        :zipcode).values.all?(&:blank?)
    attributes.merge!(_destroy: 1) if attributes_exists && attributes_empty
    !attributes_exists && attributes_empty
  end

  # Force a distinct count to work around relation count issue https://github.com/rails/rails/issues/5554
  def self.distinct_count
    count(distinct: true)
  end

  def long_description=(html)
    super(HtmlSanitizer.sanitize_and_enforce_link_target_blank(html))
  end

  def preferred_shopfront_message=(html)
    self.prefers_shopfront_message = HtmlSanitizer.sanitize_and_enforce_link_target_blank(html)
  end

  def preferred_shopfront_closed_message=(html)
    self.prefers_shopfront_closed_message =
      HtmlSanitizer.sanitize_and_enforce_link_target_blank(html)
  end

  def remove_logo=(value)
    self.logo = nil if value == "1"
  end

  def remove_promo_image=(value)
    self.promo_image = nil if value == "1"
  end

  def remove_white_label_logo=(value)
    return unless value == "1"

    self.white_label_logo = nil
    self.white_label_logo_link = nil # Link not needed if there's no logo
  end

  def contact
    contact = users.where(enterprise_roles: { receives_notifications: true }).first
    contact || owner
  end

  def contact_id
    contact&.id
  end

  def contact_id=(user_id)
    return unless user_id.to_i.positive? && users.confirmed.exists?(user_id.to_i)

    enterprise_roles.update_all(["receives_notifications=(user_id=?)", user_id])
  end

  def activated?
    contact.confirmed? && sells != 'unspecified'
  end

  def set_producer_property(property_name, property_value)
    transaction do
      property = Spree::Property.
        where(name: property_name).
        first_or_create!(presentation: property_name)
      producer_property = ProducerProperty.
        where(producer_id: id, property_id: property.id).
        first_or_initialize
      producer_property.value = property_value
      producer_property.save!
    end
  end

  def to_param
    permalink
  end

  def relatives
    Enterprise.where("
      enterprises.id IN
        (SELECT child_id FROM enterprise_relationships WHERE enterprise_relationships.parent_id=?)
      OR enterprises.id IN
        (SELECT parent_id FROM enterprise_relationships WHERE enterprise_relationships.child_id=?)
    ", id, id)
  end

  def plus_parents_and_order_cycle_producers(order_cycles)
    oc_producer_ids = Exchange.in_order_cycle(order_cycles).incoming.pluck :sender_id
    Enterprise.is_primary_producer.parents_of_one_union_others(id, oc_producer_ids | [id])
  end

  def relatives_including_self
    Enterprise.where(id: relatives.pluck(:id) | [id])
  end

  def distributors
    relatives_including_self.is_distributor
  end

  def suppliers
    relatives_including_self.is_primary_producer
  end

  def logo_url(name)
    image_url_for(logo, name)
  end

  def promo_image_url(name)
    image_url_for(promo_image, name)
  end

  def white_label_logo_url(name = :default)
    image_url_for(white_label_logo, name)
  end

  def website
    strip_url self[:website]
  end

  def facebook
    strip_url self[:facebook]
  end

  def linkedin
    strip_url self[:linkedin]
  end

  def twitter
    correct_twitter_url self[:twitter]
  end

  def instagram
    correct_instagram_url self[:instagram]
  end

  def whatsapp_url
    correct_whatsapp_url self[:whatsapp_phone]
  end

  def inventory_variants
    if prefers_product_selection_from_inventory_only?
      Spree::Variant.visible_for(self)
    else
      Spree::Variant.not_hidden_for(self)
    end
  end

  def distributed_variants
    Spree::Variant.
      joins(:product).
      merge(Spree::Product.in_distributor(self)).
      select('spree_variants.*')
  end

  def is_distributor
    sells != "none"
  end

  def is_hub
    sells == 'any'
  end

  def is_producer_only
    is_primary_producer && sells == 'none'
  end

  # Simplify enterprise categories for frontend logic and icons, and maybe other things.
  def category
    # Make this crazy logic human readable so we can argue about it sanely.
    cat = is_primary_producer ? "producer_" : "non_producer_"
    cat << "sells_#{sells}"

    # Map backend cases to front end cases.
    case cat
    when "producer_sells_any"
      :producer_hub # Producer hub who sells own and others produce and supplies other hubs.
    when "producer_sells_own"
      :producer_shop # Producer with shopfront and supplies other hubs.
    when "producer_sells_none"
      :producer # Producer only supplies through others.
    when "non_producer_sells_any", "non_producer_sells_own"
      # Hub selling others products in order cycles
      # Or Wholesaler selling through own shopfront? Does this need a separate name or even exist?
      :hub
    when "non_producer_sells_none"
      :hub_profile # Hub selling outside the system.
    end
  end

  # Return all taxons for all distributed products
  def distributed_taxons
    Spree::Taxon.
      joins(:products).
      where(spree_products: { id: Spree::Product.in_distributor(self).select(&:id) }).
      select('DISTINCT spree_taxons.*')
  end

  def current_distributed_taxons
    Spree::Taxon
      .select("DISTINCT spree_taxons.*")
      .joins(products: :variants)
      .joins("INNER JOIN (#{current_exchange_variants.to_sql}) \
        AS exchange_variants ON spree_variants.id = exchange_variants.variant_id")
  end

  # Return all taxons for all supplied products
  def supplied_taxons
    Spree::Taxon.
      joins(:products).
      where(spree_products: { id: Spree::Product.in_supplier(self).select(&:id) }).
      select('DISTINCT spree_taxons.*')
  end

  def ready_for_checkout?
    shipping_methods.frontend.any? && payment_methods.available.any?(&:configured?)
  end

  def self.find_available_permalink(test_permalink)
    test_permalink = UrlGenerator.to_url(test_permalink)
    test_permalink = "my-enterprise" if test_permalink.blank?
    existing = Enterprise.
      order(:permalink).
      where("permalink LIKE ?", "#{test_permalink}%").
      pluck(:permalink)

    if existing.include?(test_permalink)
      used_indices = existing.map do |p|
        p.slice!(/^#{test_permalink}/)
        p.match(/^\d+$/).to_s.to_i
      end.select{ |p| p }
      options = (1..existing.length).to_a - used_indices
      test_permalink + options.first.to_s
    else
      test_permalink
    end
  end

  def can_invoice?
    return true unless Spree::Config.enterprise_number_required_on_invoices?

    abn.present?
  end

  def public?
    visible == "public"
  end

  protected

  def devise_mailer
    EnterpriseMailer
  end

  private

  def validate_white_label_logo_link
    return if white_label_logo.blank?

    return if white_label_logo_link.blank?

    white_label_logo_link.strip!
    uri = URI(white_label_logo_link)
    self.white_label_logo_link = "http://#{white_label_logo_link}" if uri.scheme.nil?
  rescue URI::InvalidURIError
    errors.add(:white_label_logo_link, I18n.t(:invalid_url, url: white_label_logo_link))
  end

  def image_url_for(image, name)
    return unless image.variable?

    image_variant_url_for(image.variant(name))
  rescue StandardError => e
    Alert.raise "Enterprise ##{id} #{image.try(:name)} error: #{e.message}"
    Rails.logger.error(e.message)

    nil
  end

  def current_exchange_variants
    ExchangeVariant.joins(exchange: :order_cycle)
      .merge(Exchange.outgoing)
      .select("DISTINCT exchange_variants.variant_id, exchanges.receiver_id AS enterprise_id")
      .where(exchanges: { receiver_id: id })
      .merge(OrderCycle.active.with_distributor(id))
  end

  def name_is_unique
    dups = Enterprise.where(name:)
    dups = dups.where.not(id:) unless new_record?

    errors.add :name, I18n.t(:enterprise_name_error, email: dups.first.owner.email) if dups.any?
  end

  def send_welcome_email
    EnterpriseMailer.welcome(self).deliver_later
  end

  def strip_url(url)
    # Strip protocol and trailing slash
    url&.sub(%r{(https?://)?}, '')&.sub(%r{/\z}, '')
  end

  def correct_whatsapp_url(phone_number)
    phone_number && "https://wa.me/#{phone_number.tr('+ ', '')}"
  end

  def correct_instagram_url(url)
    url && strip_url(url.downcase).sub(%r{(www\.)?instagram.com/}, '').delete("@")
  end

  def correct_twitter_url(url)
    url && strip_url(url).sub(%r{(www\.)?twitter.com/}, '').delete("@")
  end

  def set_unused_address_fields
    if address.present?
      address.firstname = address.lastname = address.phone =
                            address.company = 'unused'
    end
    business_address.first_name = business_address.last_name = 'unused' if business_address.present?
  end

  def ensure_owner_is_manager
    users << owner unless users.include?(owner)
  end

  def enforce_ownership_limit
    return if owner.can_own_more_enterprises?

    errors.add(:owner, I18n.t(:enterprise_owner_error, email: owner.email,
                                                       enterprise_limit: owner.enterprise_limit ))
  end

  def set_default_contact
    self.contact_id = owner_id
  end

  def relate_to_owners_enterprises
    # When a new producer is created, it grants permissions to all pre-existing hubs
    # When a new hub is created,
    # - it grants permissions to all pre-existing hubs
    # - all producers grant permission to it

    enterprises = owner.owned_enterprises.where.not(enterprises: { id: self })

    # We grant permissions to all pre-existing hubs
    hub_permissions = [:add_to_order_cycle]
    hub_permissions << :create_variant_overrides if is_primary_producer
    enterprises.is_hub.each do |enterprise|
      EnterpriseRelationship.create!(parent: self,
                                     child: enterprise,
                                     permissions_list: hub_permissions)
    end

    # All pre-existing producers grant permission to new hubs
    return unless is_hub

    enterprises.is_primary_producer.each do |enterprise|
      EnterpriseRelationship.create!(parent: enterprise,
                                     child: self,
                                     permissions_list: [:add_to_order_cycle,
                                                        :create_variant_overrides])
    end
  end

  def shopfront_taxons
    return if preferred_shopfront_taxon_order =~ /\A((\d+,)*\d+)?\z/

    errors.add(:shopfront_category_ordering, "must contain a list of taxons.")
  end

  def shopfront_producers
    return if preferred_shopfront_producer_order =~ /\A((\d+,)*\d+)?\z/

    errors.add(:shopfront_category_ordering, "must contain a list of producers.")
  end

  def restore_permalink
    # If the permalink has errors, reset it to it's original value, so we can update the form
    self.permalink = permalink_was if permalink_changed? && errors[:permalink].present?
  end

  def initialize_permalink
    return unless name

    self.permalink = Enterprise.find_available_permalink(name)
  end

  # Touch distributors without them touching their distributors.
  # We avoid an infinite loop and don't need to touch the whole distributor tree.
  def touch_distributors
    Enterprise.distributing_variants(supplied_variants.select(:id)).
      where.not(enterprises: { id: }).
      update_all(updated_at: Time.zone.now)
  end
end
