# frozen_string_literal: true

require 'cgi'
require 'json'

class UxToolkitUserPreference < ApplicationRecord
  LANDING_PAGE_TYPE_MY_PAGE = 'my_page'
  LANDING_PAGE_TYPE_MY_WORK = 'my_work'
  LANDING_PAGE_TYPE_CUSTOM_URL = 'custom_url'
  LANDING_PAGE_TYPES = [
    LANDING_PAGE_TYPE_MY_PAGE,
    LANDING_PAGE_TYPE_MY_WORK,
    LANDING_PAGE_TYPE_CUSTOM_URL
  ].freeze
  DASHBOARD_BLOCK_ASSIGNED_TO_ME = 'assigned_to_me'
  DASHBOARD_BLOCK_DUE_TODAY = 'due_today'
  DASHBOARD_BLOCK_OVERDUE = 'overdue'
  DASHBOARD_BLOCK_DUE_SOON = 'due_soon'
  DASHBOARD_BLOCK_FAVORITE_PROJECTS = 'favorite_projects'
  DASHBOARD_BLOCK_FAVORITE_QUERIES = 'favorite_queries'
  DASHBOARD_BLOCK_RECENT_ISSUES = 'recent_issues'
  DASHBOARD_BLOCK_RECENT_PROJECTS = 'recent_projects'
  DASHBOARD_BLOCKS = [
    DASHBOARD_BLOCK_ASSIGNED_TO_ME,
    DASHBOARD_BLOCK_DUE_TODAY,
    DASHBOARD_BLOCK_OVERDUE,
    DASHBOARD_BLOCK_DUE_SOON,
    DASHBOARD_BLOCK_FAVORITE_PROJECTS,
    DASHBOARD_BLOCK_FAVORITE_QUERIES,
    DASHBOARD_BLOCK_RECENT_ISSUES,
    DASHBOARD_BLOCK_RECENT_PROJECTS
  ].freeze
  DASHBOARD_BLOCK_LABELS = {
    DASHBOARD_BLOCK_ASSIGNED_TO_ME => :label_ux_toolkit_assigned_to_me,
    DASHBOARD_BLOCK_DUE_TODAY => :label_ux_toolkit_due_today,
    DASHBOARD_BLOCK_OVERDUE => :label_ux_toolkit_overdue,
    DASHBOARD_BLOCK_DUE_SOON => :label_ux_toolkit_due_soon,
    DASHBOARD_BLOCK_FAVORITE_PROJECTS => :label_ux_toolkit_favorite_projects,
    DASHBOARD_BLOCK_FAVORITE_QUERIES => :label_ux_toolkit_favorite_issue_filters,
    DASHBOARD_BLOCK_RECENT_ISSUES => :label_ux_toolkit_recently_visited_issues,
    DASHBOARD_BLOCK_RECENT_PROJECTS => :label_ux_toolkit_recently_visited_projects
  }.freeze

  belongs_to :user

  validates :user_id, presence: true, uniqueness: true
  validates :landing_page_type, inclusion: {in: LANDING_PAGE_TYPES, allow_blank: true}
  validate :validate_landing_page_value

  before_validation :normalize_blank_landing_page_type
  before_validation :normalize_landing_page_value

  def self.valid_internal_path?(value)
    path = value.to_s.strip
    return false if path.blank?
    return false unless path.start_with?('/')
    return false if path.start_with?('//')
    return false if path.match?(%r{(?:\A|[/?#&=])[A-Za-z][A-Za-z0-9+\-.]*:})

    decoded_path = repeatedly_unescape(path)
    return false if decoded_path.blank?
    return false if decoded_path.include?('..')
    return false if decoded_path.include?('\\')

    true
  end

  def resolved_landing_path
    case landing_page_type
    when LANDING_PAGE_TYPE_MY_PAGE
      '/my/page'
    when LANDING_PAGE_TYPE_MY_WORK
      '/my/work'
    when LANDING_PAGE_TYPE_CUSTOM_URL
      self.class.valid_internal_path?(landing_page_value) ? landing_page_value : nil
    end
  end

  def self.dashboard_block_label_key(block)
    DASHBOARD_BLOCK_LABELS.fetch(block.to_s)
  end

  def self.default_dashboard_block_keys(settings = nil)
    settings ||= Setting.plugin_redmine_ux_toolkit || {}
    raw_value = settings['default_my_work_dashboard_blocks']

    sanitize_dashboard_block_keys(raw_value, fallback: DASHBOARD_BLOCKS)
  end

  def self.sanitize_dashboard_block_keys(raw_value, fallback:)
    return fallback if raw_value.blank?

    keys =
      if raw_value.is_a?(Array)
        raw_value
      else
        parsed_value = JSON.parse(raw_value.to_s)
        return fallback unless parsed_value.is_a?(Array)

        parsed_value
      end

    sanitized_keys = keys.map(&:to_s) & DASHBOARD_BLOCKS
    sanitized_keys.presence || fallback
  rescue JSON::ParserError, TypeError
    fallback
  end

  def personal_dashboard_block_preference?
    !self[:visible_dashboard_blocks].nil?
  end

  def visible_dashboard_block_keys(default_blocks: self.class::DASHBOARD_BLOCKS)
    raw_value = self[:visible_dashboard_blocks]
    return default_blocks if raw_value.nil?

    keys = JSON.parse(raw_value)
    return DASHBOARD_BLOCKS unless keys.is_a?(Array)

    keys.map(&:to_s) & DASHBOARD_BLOCKS
  rescue JSON::ParserError, TypeError
    DASHBOARD_BLOCKS
  end

  def resolved_visible_dashboard_block_keys
    visible = visible_dashboard_block_keys(default_blocks: self.class.default_dashboard_block_keys)
    resolved_dashboard_block_order.select { |block| visible.include?(block) }
  end

  def visible_dashboard_block_keys=(keys)
    self[:visible_dashboard_blocks] = (Array(keys).map(&:to_s) & DASHBOARD_BLOCKS).to_json
  end

  def resolved_dashboard_block_order
    raw_value = self[:dashboard_block_order]
    stored_order = raw_value.present? ? JSON.parse(raw_value) : []
    sanitized_order = Array(stored_order).map(&:to_s) & DASHBOARD_BLOCKS

    sanitized_order + (DASHBOARD_BLOCKS - sanitized_order)
  rescue JSON::ParserError, TypeError
    DASHBOARD_BLOCKS
  end

  def dashboard_block_order=(keys)
    sanitized_order = Array(keys).map(&:to_s) & DASHBOARD_BLOCKS
    self[:dashboard_block_order] = (sanitized_order + (DASHBOARD_BLOCKS - sanitized_order)).to_json
  end

  def dashboard_block_visible?(block)
    visible_dashboard_block_keys.include?(block.to_s)
  end

  private

  def self.repeatedly_unescape(value)
    decoded = value.to_s

    2.times do
      next_decoded = CGI.unescape(decoded)
      break if next_decoded == decoded

      decoded = next_decoded
    end

    decoded
  rescue ArgumentError
    nil
  end
  private_class_method :repeatedly_unescape

  def normalize_blank_landing_page_type
    self.landing_page_type = nil if landing_page_type.blank?
  end

  def normalize_landing_page_value
    self.landing_page_value = landing_page_value.to_s.strip if landing_page_value.present?
    self.landing_page_value = nil if landing_page_value.blank?
    self.landing_page_value = nil unless landing_page_type == LANDING_PAGE_TYPE_CUSTOM_URL
  end

  def validate_landing_page_value
    case landing_page_type
    when LANDING_PAGE_TYPE_CUSTOM_URL
      unless self.class.valid_internal_path?(landing_page_value)
        errors.add(:landing_page_value, I18n.t(:error_ux_toolkit_invalid_internal_url))
      end
    when LANDING_PAGE_TYPE_MY_PAGE, LANDING_PAGE_TYPE_MY_WORK, nil
      true
    end
  end
end
