require "history_client"

module Twitch
  class User < Twitch::Base
    attributes :id, :login, :email, :deleted_on, :terms_of_service_violation,
               :dmca_violation, :description, :email_verified, :location, :remote_ip,
               :category, :delay, :language, :birthday, :last_login, :created_on,
               :admin, :subadmin, :global_mod, :displayname,
               :tos_violation_count, :dmca_violation_count, :banned_until,
               :enable_email_reuse, :phone_number, :deletion_hold, :username_flagged_on,
               :twofa_info, :authy_last_four_digits, :authy_country_code, :authy_region_code

    self.primary_attribute = :id

    validates :id, :login, :email, presence: true

    AUDIT_ACTION_UPDATE = "update"
    AUDIT_ACTION_BAN = "ban_user"
    AUDIT_ACTION_PERMABAN = "permaban_user"
    AUDIT_ACTION_UNBAN = "unban_user"
    AUDIT_TWITCH_USER = "twitch_user"
    AUDIT_LDAP_USER = "ldap_user"
    AUDIT_CHANGE_LOGIN = "login"
    AUDIT_CHANGE_BANNED_UNTIL = "banned_until"
    AUDIT_CHANGE_TOS_STRIKES = "tos_violation_count"
    AUDIT_CHANGE_DMCA_STRIKES = "dmca_violation_count"

    RENAME_OVERRIDE_FLAGGED_FOR_RENAME = <<~OVERRIDE
      This account is currently deactivated, pending a required username reset at next login.
      Continuing with this rename will lift the reset requirement and reactivate the account for use.

      Note: Accounts currently under suspension will remain suspended after this rename.
    OVERRIDE

    # available user statuses
    STATUS_SUSPENDED_TOS  = 'TOS Banned'
    STATUS_SUSPENDED_DMCA = 'DMCA Banned'
    STATUS_BLOCK_RECORD   = 'Block Record'
    STATUS_DEACTIVATED    = 'Deactivated'
    STATUS_RENAME_PENDING = 'Pending Rename'
    STATUS_ACTIVE         = 'Active'

    # attribute validation (via shadowing)
    # @see ADMIN-1557
    def remote_ip
      @remote_ip.try(:split, ',')&.first
    end

    def self.all(options = {})
      params = make_params(options[:where])
      [:id, :ip, :email].each do |field|
        params[field] = options[:where][field] if options[:where][field].present?
      end

      response = get "/users", params: params

      return empty_response unless response.success?

      dupe_users = response.body["results"].map do |users_attributes|
        from_attributes(users_attributes)
      end

      users = dupe_users.uniq(&:id)
      if options[:skip_pagination]
        return paginate users
      end

      page = (options[:page].presence || 1).to_i
      per_page = (options[:per_page].presence || PER_PAGE).to_i
      users_count = users.length
      index = (page.to_i - 1) * per_page
      users = users.sort_by(&:login)[index...index + per_page]

      paginate users, per_page: per_page, total_count: users_count
    end

    def self.find(id, options = {})
      begin
        response = get "/users", params: {id: id}
      rescue StandardError => err
        # log an error so we can dig into cloudwatch
        Service::StructuredLogger.error(
          'users_service_error',
          name: err.class.name,
          message: err.message,
          backtrace: err.backtrace
        )
        # raise the exception so it makes it to rollbar
        raise err
      end

      # raise service errors as an exception if the response body actually contains one
      raise response.body["message"] unless response.success? || !response.body["message"]

      # raise the entire body contents if we somehow made it through the previous err check

      unless response.body["results"] && response.success?
        raise response
          .to_hash
          .map { |k, v| k == :request_headers ? [k, v.reject! {|sk, _v| sk == "Twitch-Service-Auth-Req-Signature"}] : [k, v] }
          .to_h
          .inspect
      end

      user_attributes = response.body["results"].first
      from_attributes(user_attributes) if user_attributes.present?
    end

    def self.find_by_login(login, _options = {})
      begin
        response = get "/users", params: {login: login}
      rescue StandardError => err
        # log an error so we can dig into cloudwatch
        Service::StructuredLogger.error(
          'users_service_error',
          name: err.class.name,
          message: err.message,
          backtrace: err.backtrace
        )
        # raise the exception so it makes it to rollbar
        raise err
      end

      # raise service errors as an exception if the response body actually contains one
      raise response.body["message"] unless response.success? || !response.body["message"]

      # raise the entire body contents if we somehow made it through the previous err check

      unless response.body["results"] && response.success?
        raise response
          .to_hash
          .map { |k, v| k == :request_headers ? [k, v.reject! {|sk, _v| sk == "Twitch-Service-Auth-Req-Signature"}] : [k, v] }
          .to_h
          .inspect
      end

      user_attributes = response.body["results"].first
      from_attributes(user_attributes) if user_attributes.present?
    end

    def self.empty_response
      paginate []
    end

    def audit_rename(options = {})
      changes = []

      changes << History::ChangeSet.new(
        attribute: AUDIT_CHANGE_LOGIN,
        old_value: options[:old_login],
        new_value: options[:new_login]
      )

      if options[:flagged_for_rename].present?
        changes << History::ChangeSet.new(
          attribute: "username_flagged_on",
          new_value: options[:flagged_for_rename]
        )
      end

      if options[:appeal_audit_id].present?
        changes << History::ChangeSet.new(
          attribute: "appeal_audit_id",
          old_value: options[:appeal_audit_id]
        )
      end

      if options[:username_change_audit_id].present?
        changes << History::ChangeSet.new(
          attribute: "username_change_audit_id",
          new_value: options[:username_change_audit_id]
        )
      end

      audit = History::Audit.new(
        action: AUDIT_ACTION_UPDATE,
        user_type: AUDIT_LDAP_USER,
        user_id:  options[:ldap_login],
        resource_type: AUDIT_TWITCH_USER,
        resource_id: fetch_id.to_s,
        description: options[:rename_reason],
        changes: changes
      )

      History::AddAudit.add(audit)
    end

    def safety_rename_audit(old_login:, new_login:, user_id:, admin_ldap:)
      safety_appeal_audit =
        SupportTools::AegisGateway
        .create_force_username_reset_audit(
          action: :appeal_successful,
          login: old_login,
          user_id: user_id,
          ldap: admin_ldap
        )

      safety_username_change_audit =
        SupportTools::AegisGateway
        .create_force_username_reset_audit(
          action: :username_changed,
          login: new_login,
          user_id: user_id,
          ldap: admin_ldap
        )

      {
        appeal_audit_id: safety_appeal_audit.try(:audit_id),
        username_change_audit_id: safety_username_change_audit.try(:audit_id)
      }
    end

    def update(options)
      user = options[:twitch_user]
      return false if user.blank?

      args = {
          description: user[:description],
          displayname: user[:displayname],
          email: user[:email],
          phone_number: user[:phone_number] != "" ? user[:phone_number] : nil,
          category: user[:category],
          birthday: user[:birthday] != "" ? Time.parse(user[:birthday]) : nil,
          language: user[:language],
          override_displayname: true,
          deletion_hold: user[:deletion_hold] == '1',
          override_email_reuse: true,
          skip_user_notification: true,
          override_email_cooldown: true,
      }

      parsed_email_reuse = user[:enable_email_reuse] == '1'

      # can only set this param if user has a verified email
      args[:enable_email_reuse] = parsed_email_reuse if email_verified

      body = args.to_json

      response =
        if args[:phone_number].blank? && phone_number.present?
          patch "/users/#{user[:id]}", body: { delete_phone_number: true }.to_json
        else
          patch "/users/#{user[:id]}", body: body
        end

      response
    end

    def rename(options)
      user_id = fetch_id
      return false if user_id.blank?

      options[:old_login] = login

      # flags used for every admin panel rename request
      override_flags = {
        skip_user_notification: true,
        override_login_block: options[:admin_override].present?,
        override_login_length: options[:admin_override].present?,
        skip_name_validation: options[:admin_override].present?
      }

      if flagged_for_rename?

        # we fake an error to force the override flow with
        # our desired messaging
        if options[:admin_override].blank?
          self.errors = RENAME_OVERRIDE_FLAGGED_FOR_RENAME
          return false
        end

        # set up the rename flag data for auditing later
        options[:flagged_for_rename] = username_flagged_on

        reset = {
          id: user_id.to_s, # users service generally expects userIDs to be a string
          admin: options[:ldap_login],
          login: options["new_login"].strip
        }.merge(override_flags).to_json

        response = patch "/username_reset", body: reset

      else

        rename = {
          new_login: options["new_login"].strip,
          skip_login_cooldown: true
        }.merge(override_flags).to_json

        response = patch "/users/#{user_id}", body: rename
      end

      if response.success?
        audit_ids = safety_rename_audit(
          old_login: options[:old_login],
          new_login: options["new_login"].strip,
          user_id: user_id,
          admin_ldap: options[:ldap_login]
        )
        audit_rename(options.merge(audit_ids))
      else
        self.errors = response.body["message"]
      end

      response.success?
    end

    def update_channel(options)
      channel = options[:twitch_user]
      return false if channel.blank?

      id = options[:id]
      return false if id.blank?

      body = {
          id: id.to_i,
          mature: true?(channel[:mature_channel]),
          redirect_channel: channel[:redirect_channel],
          directory_hidden: true?(channel[:directory_hidden]),
          privacy_options_enabled: true?(channel[:privacy_options_enabled]) || true?(channel[:private_video]), # Privacy options must be enabled if private_video == true
          private_video: true?(channel[:private_video])
      }.to_json

      response = patch "/channels?id=#{id}", body: body
      response
    end

    def if_user_exists(login)
      begin
        response = get "/logins?login=#{login}"
      rescue StandardError => err
        # log an error so we can dig into cloudwatch
        Service::StructuredLogger.error(
          'users_service_error',
          name: err.class.name,
          message: err.message,
          backtrace: err.backtrace
        )
        # raise the exception so it makes it to rollbar
        raise err
      end

      # raise service errors as an exception if the response body actually contains one
      raise response.body["message"] unless response.success? || !response.body["message"]

      # raise the entire body contents if we somehow made it through the previous err check

      unless response.body["results"] && response.success?
        raise response
          .to_hash
          .map { |k, v| k == :request_headers ? [k, v.reject! {|sk, _v| sk == "Twitch-Service-Auth-Req-Signature"}] : [k, v] }
          .to_h
          .inspect
      end
      response.body["results"].each do |result|
        return true if result['id'].blank?
      end
      false
    end

    def status
      statuses = []

      statuses << STATUS_SUSPENDED_TOS if terms_of_service_violation
      statuses << STATUS_SUSPENDED_DMCA if dmca_violation
      statuses << STATUS_BLOCK_RECORD if block_record?
      statuses << STATUS_DEACTIVATED if statuses.blank? && deleted_on
      statuses << STATUS_RENAME_PENDING if flagged_for_rename?
      statuses << STATUS_ACTIVE if statuses.blank?

      statuses
    end

    def self.from_attributes(params)
      super(params.slice(*self.attribute_names))
    end

    def self.connection
      super(params_encoder: Faraday::FlatParamsEncoder)
    end

    def self.make_params(conditions)
      text = conditions.delete(:text)
      return {} if text.blank?

      keys_regex = {
        id: /^\d+$/,
        login: /^[a-zA-Z0-9_]+$/,
        email: /@[a-z0-9.\-]+\.[a-z]{2,24}$/,
        ip: /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/,
        phone_number: /^\+[0-9]+$/,
      }

      params = keys_regex.inject({}) do |_, (key, regex)|
        _[key] = text if text =~ regex
        _
      end

      params[:displayname] = text
      params.merge! conditions

      params
    end

    def fetch_id
      return self.id if self.id.present?
      response = get "/users", params: {login: login}
      return nil unless response.success? && response.body["results"].present?
      self.id = response.body["results"].first["id"]
    end

    def audit_tos_action(action, options)
      changes = []

      if options[:decrement] && options[:types]
        if (options[:decrement]).positive? && options[:types].include?("tos")
          changes.push(
            History::ChangeSet.new(
              attribute: AUDIT_CHANGE_TOS_STRIKES,
              old_value: self.tos_violation_count,
              new_value: [self.tos_violation_count.to_i - options[:decrement], 0].max
            )
          )
        end

        if (options[:decrement]).positive? && options[:types].include?("dmca")
          changes.push(
            History::ChangeSet.new(
              attribute: AUDIT_CHANGE_DMCA_STRIKES,
              old_value: self.dmca_violation_count,
              new_value: [self.dmca_violation_count.to_i - options[:decrement], 0].max
            )
          )
        end
      end

      audit = History::Audit.new(
        action: action,
        user_type: AUDIT_LDAP_USER,
        user_id: options["reporter"],
        resource_type: AUDIT_TWITCH_USER,
        resource_id: fetch_id.to_s,
        description: options["description"],
        changes: changes
      )

      History::AddAudit.add(audit)
    end

    def tos_ban(options)
      user_id = fetch_id
      return false if user_id.blank?

      is_perma_ban = self.class.booleanize(options["is_permanent"])
      audit_action = is_perma_ban ? AUDIT_ACTION_PERMABAN : AUDIT_ACTION_BAN

      success = SupportTools::AegisGateway.create_enforcement({
        content: options["content"],
        description: options["description"],
        detailed_reason: options["detailed_reason"],
        from_user_id: options["reporter"],
        ip_ban: !self.class.booleanize(options["skip_ip_ban"]),
        origin: "web/admin-panel",
        permanent: is_perma_ban,
        reason: options["reason"],
        target_user_user_id: user_id.to_s,
      })
      audit_tos_action(audit_action, options) if success

      if success && options["reason"] == "fraud" && options["content"] == "purchase" # Refund purchases for last 60 days if user is banned for purchase fraud
        success = Payments::RefundAllPayments::refund_all_payments(user_id, options["reporter"], options["reporter_email"], 60, options["description"])
      end

      success
    end

    def tos_unban(options)
      user_id = fetch_id
      return false if user_id.blank?

      if options[:decrement] && options[:types]
        tos_query = {
          :decrement => options[:decrement],
          :ban_type => (options[:types].include?("tos") ? "tos" : "")
        }.to_query
        dmca_query = {
          :ban_type => (options[:types].include?("dmca") ? "dmca" : "")
        }.to_query
      end

      response = delete "/users/#{user_id}/ban?#{tos_query}&#{dmca_query}"
      audit_tos_action(AUDIT_ACTION_UNBAN, options) if response.success?
      response.success?
    end

    def soft_delete(admin, flagged_for_rename = false)
      user_id = fetch_id
      return false if user_id.blank?

      url = "/users/#{user_id}?admin=#{admin}"

      # unreachable for now ( #1220 )
      if flagged_for_rename
        url << '&is_rename=true'
      end

      delete url
    end

    def hard_delete(admin)
      user_id = fetch_id
      return false if user_id.blank?

      url = "/users/#{user_id}?admin=#{admin}&destroy=true"

      delete url
    end

    def undelete(admin)
      user_id = fetch_id
      return false if user_id.blank?

      url = "/users/#{user_id}/undelete?admin=#{admin}"

      response = patch url

      if response.success?
        SupportTools::AegisGateway
          .create_force_username_reset_audit(
            action: :appeal_successful,
            login: login,
            user_id: user_id,
            ldap: admin
          )
      end

      response
    end

    def force_password_reset
      Passport::Passwords.force_reset(self.id)
    end

    def change_password(password)
      Passport::Passwords.update_password(self.id, password)
    end

    def delete_all_cookies
      Sessions::Cookies.delete_all(self.id)
    end

    def delete_all_authorizations
      authorizations = OauthClients::Authorization.all(self.id)
      authorizations.each do |authorization|
        success = authorization.destroy
        return false unless success
      end
      true
    end

    def does_ip_match_historic_ips(candidate_ip)
      historic_ips = History::QueryAudit.ips_for_twitch_user(self.id)

      return IpComparison.compare_ips(candidate_ips, historic_ips)
    end

    def email_count
      return nil if email.blank?

      response = get "/users", params: {email: email}
      response.body["results"].count if response.success?
    end

    def ip_count
      return nil if remote_ip.blank?

      response = get "/users", params: {ip: remote_ip}
      response.body["results"].count if response.success?
    end

    def user_vods
      @vods ||= Vods::Channel.find_for_user(id: self.id)
    end

    def vods_count
      user_vods.try(:total_count) || 'Unknown'
    end

    def payout_entity(ldap_login = "")
      options = {ldap_login: ldap_login}
      @payout_entity ||= PayoutEntities::PayoutEntity.find(self.id, options: options, find_by_channel_id: true)
    end

    def live_payout_entity_id
      PayoutEntities::PayoutEntity.live_payout_entity_id(self.id)
    end

    def payout_type
      @payout_type ||= PayoutEntities::PayoutEntity.payout_type(self.id)
    end

    def partner_program?
      payout_type["is_partner"].present?
    end

    def affiliate?
      payout_type["is_affiliate"].present?
    end

    def developer?
      payout_type["is_developer"].present?
    end

    def partner_terms
      PayoutEntities::Invitation.partner_terms_from_tags(payout_type["tags"])
    end

    def zero_cpm?
      payout_type["category"].include? PayoutEntities::Invitation::CATEGORY_0CPM
    end

    def validated_non_partner?
      Ripley::ValidatedNonPartner.find(self.id.to_s)
    end

    def esports?
      tags = payout_type["tags"]
      return tags.present? && tags.include?("esports")
    end

    def legacy?
      tags = payout_type["tags"]
      return tags.present? && tags.include?("legacy")
    end

    def invitations
      return @invitations if @invitations

      @invitations = PayoutEntities::Invitation.invitations(self.id)
      @invitations.each do |invitation|
        invitation["terms"] = PayoutEntities::Invitation.partner_terms_from_tags(invitation["tags"])
      end
      @invitations
    end

    def bits_balance
      @response ||= Bits::User.bits_balance(self.id)
      return {} unless @response.success?
      return @response.body
    end

    def bits_count
      bits_balance["balance"].to_i
    end

    def tmi_room_info
      @tmi_room_info ||= Tmi::Chat.room_info(self.id)
    end

    def twitchbot_rule_id
      tmi_room_info["twitchbot_rule_id"]
    end

    def chat_delay_duration
      tmi_room_info["chat_delay_duration"]
    end

    def tmi_bot_info
      @tmi_bot_info = Tmi::Bot.bot_info(self.id)
    end

    def is_verified_bot?
      tmi_bot_info["is_verified_bot"]
    end

    def twofa_info
      unless self.errors.include?(__method__)
        @twofa_info ||= Passport::TwoFactor.get_2fa(user_id: self.id)
        # If a user's authy account has been deleted, passport will still have 2FA enabled
        # and attached to the user, so we need to get the authy ID a different way
        if @twofa_info.errors.present?
          twofa_status = Passport::TwoFactor.get_passport_2fa(user_id: self.id)
          @twofa_info.errors.each do |attribute, err|
            twofa_status.errors.add(attribute, err) unless twofa_status.errors.added?(attribute, err)
          end
          @twofa_info = twofa_status
        end
        @twofa_info
      end
    end

    def authy_id
      twofa_info.try(:authy_id)
    end

    def authy_last_four_digits
      unless self.errors.include?(__method__)
        twofa_info.try(:last_four_digits)
      end
    end

    def authy_country_code
      unless self.errors.include?(__method__)
        twofa_info.try(:country_code)
      end
    end

    def authy_region_code
      unless self.errors.include?(__method__)
        twofa_info.try(:region_code)
      end
    end

    def authy_created_at
      ts = twofa_info.try(:created_at)
      Time.at(ts).utc unless ts.nil?
    end

    def authy_updated_at
      ts = twofa_info.try(:updated_at)
      Time.at(ts).utc unless ts.nil?
    end

    def disconnect_chat_sessions
      Clue::User.disconnect(self.id)
    end

    def chat_ip_abuse_status
      @chat_ip_abuse_status ||= Tmi::Chat.chat_ip_abuse_status(self.remote_ip)
    end

    def chat_ip_blocked?
      chat_ip_abuse_status["blocked"]
    end

    def chat_bans
      @chat_bans ||= Tmi::Chat.chat_bans(self.id)
    end

    def chat_bans_count
      chat_bans["_total"]
    end

    def followers_count
      @followers_count ||= ::Follows::Follower.count(self.id)
    end

    def vod_property
      @vod_property ||= Vods::UserVodProperty.find(self.id)
    end

    def save_vods_forever?
      vod_property.try(:save_vods_forever)
    end

    def skip_upload_moderation?
      vod_property.try(:skip_upload_moderation)
    end

    def can_upload_unlimited?
      vod_property.try(:can_upload_unlimited)
    end

    def skip_muting?
      vod_property.try(:skip_muting)
    end

    def vod_privacy_property
      @vod_privacy_property ||= Vods::UserVodPrivacyProperty.find(self.id)
    end

    def hide_archives?
      vod_privacy_property.try(:hide_archives)
    end

    def hide_archives_enabled?
      vod_privacy_property.try(:hide_archives_enabled)
    end

    def channel_results
      @channel_results ||= Channels::Channel.properties(self.id)
    end

    def can_rebroadcast?
      channel_results["can_rebroadcast"].present?
    end

    def delay_controls_enabled?
      channel_results["delay_controls_enabled"].present?
    end

    def redirect_channel
      channel_results["redirect_channel"]
    end

    def mature_channel?
      channel_results["mature"].present?
    end

    def directory_hidden?
      channel_results["directory_hidden"].present?
    end

    def privacy_options_enabled?
      channel_results["privacy_options_enabled"].present?
    end

    def private_video?
      channel_results["private_video"].present?
    end

    def accounts_with_same_email
      @email_count ||= email_count
    end

    def accounts_with_same_ip
      @ip_count ||= ip_count
    end

    def ad_properties
      Ads::Setting.find(self.id)
    end

    def auto_ads
      Ads::AutoAd.find(self.id.to_s)
    end

    def purchase_profiles_count
      Payments::PurchaseProfile.count_for_purchaser(id)
    end

    def purchase_orders_count
      Payments::PurchaseOrder.count_for_purchaser(id)
    end

    def wallet_currencies
      wallet = []
      begin
        wallet = TwitchNami::Balance
                 .call(id.to_s) # nami generally expects userIDs to be a string
                 .user_balances # fetch the result's user balances
                 .reject { |b| b.amount.zero? } # reject any zero-sum currencies
                 .map(&:currency) # transform the current entry into the currency code
      rescue StandardError => err
        # log an error so we can dig into cloudwatch
        Service::StructuredLogger.error(
          'nami_service_error',
          name: err.class.name,
          message: err.message,
          backtrace: err.backtrace
        )
      end

      wallet << 'None' if wallet.blank?

      @wallet ||= wallet
    end

    def two_factor_required?
      self.admin || self.subadmin || self.global_mod
    end

    def revenue_configs
      @revenue_configs ||= Ads::RevenueConfig.find(self.id)
    end

    def two_factor_enabled?
      authy_id.present?
    end

    def two_factor_errors
      errors =
        twofa_info
        .try(:errors)
        &.full_messages
        &.map {|e| e unless e.respond_to?(:map)}
        &.compact
        &.to_sentence

      return errors unless errors == Passport::Base::ERR_MFA_NOT_ENABLED
      ""
    end

    def flagged_for_rename?
      deleted_on.present? && username_flagged_on.present?
    end

    def block_record?
      deleted_on && !email && deleted_on < created_on
    end

    def channel_property
      @channel_property ||= Twitch::UserChannelProperty.find(self.id)
    end

    def dmca_strikes_received(begin_time, end_time)
      window_start = begin_time.strftime("%F %T")
      window_end = end_time.strftime("%F %T")
      Dmca::Takedown.find_received(self.id.to_s, window_start, window_end)
    end

    def disable_twitchguard!
      Passport::TwitchGuard.disable(self.id.to_s)
    end

    def find_rbac_memberships
      Rbac::Membership.find_memberships(self.id.to_s)
    end

    def true?(obj)
      obj.to_s == "true" || obj.to_s == "1"
    end
  end
end
